Compare commits

...

10 commits

Author SHA1 Message Date
034215bf36 Add png 2025-05-18 17:25:05 +09:00
f1cc49caf8 Update menus 2025-05-18 15:34:40 +09:00
614e1fc865 Update menu 2025-05-17 09:25:53 +09:00
b3c0bf53a4 Implementing main view and main menu 2025-05-16 08:35:53 +09:00
dead6c133c Rename gui to app 2025-05-16 07:01:46 +09:00
de3708a291 Split migration to client, server and core 2025-05-15 08:43:34 +09:00
a0356c5ed0 Fix errors 2025-05-15 07:28:06 +09:00
40450c72fe Create crate for gui 2025-05-14 08:01:14 +09:00
381f3ffcfc Merge cli to client 2025-05-13 19:43:52 +09:00
5700d19a85 Add feature flag for compile wasm 2025-05-13 18:36:58 +09:00
72 changed files with 5398 additions and 1258 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.png filter=lfs diff=lfs merge=lfs -text

3831
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -8,15 +8,16 @@ chrono-tz = {version = "0.10.3", features = ["serde"]}
clap = {version = "4.5", features = ["derive"]}
dirs = "6.0.0"
dotenv = "0.15.0"
progress-pile-cli.path = "progress-pile-cli"
progress-pile-client.path = "progress-pile-client"
progress-pile-core = { path = "progress-pile-core" }
progress-pile-migration = { path = "progress-pile-migration", default-features = false }
progress-pile-client = { path = "progress-pile-client", default-features = false }
progress-pile-core = { path = "progress-pile-core", default-features = false }
progress-pile-migration-client.path = "progress-pile-migration-client"
progress-pile-migration-core.path = "progress-pile-migration-core"
progress-pile-migration-server.path = "progress-pile-migration-server"
progress-pile-server.path = "progress-pile-server"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.20.0"
thiserror = "2.0.12"
tokio = "1.44.2"
tokio = {version = "1.44.2", features = ["macros", "sync"] }
toml = "0.8.22"
uuid = { version = "1.16.0", features = [ "serde", "v4" ] }

View file

@ -0,0 +1,13 @@
[package]
name = "progress-pile-app"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[features]
desktop = ["progress-pile-client/desktop"]
[dependencies]
bevy.version = "0.16.0"
progress-pile-client.workspace = true

BIN
progress-pile-app/assets/textures/Game Icons/exitRight.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
progress-pile-app/assets/textures/Game Icons/right.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
progress-pile-app/assets/textures/Game Icons/wrench.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,56 @@
mod plugins;
pub mod themes;
use bevy::prelude::*;
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
enum AppState {
#[default]
Loading,
MainView,
Menu,
List,
Graph
}
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
enum DisplayQuality {
Low,
Medium,
High,
}
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
struct Volume(u32);
#[derive(Resource, Debug, Component, PartialEq, Eq, Clone, Copy)]
enum ViewerMode {
PokerTip2D,
PokerTip3D,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(DisplayQuality::Medium)
.insert_resource(Volume(7))
.insert_resource(ViewerMode::PokerTip2D)
.init_state::<AppState>()
.add_systems(Startup, setup)
.add_plugins((plugins::loading::loading_plugin, plugins::main_menu::menu_plugin, plugins::main_view::main_view_plugin))
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
}
// Generic system that takes a component as a parameter, and will despawn all entities with that component
fn despawn_screen<T: Component>(to_despawn: Query<Entity, With<T>>, mut commands: Commands) {
for entity in &to_despawn {
commands.entity(entity).despawn();
}
}

View file

@ -0,0 +1,59 @@
use bevy::prelude::*;
use crate::{despawn_screen, AppState};
// This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu
pub fn loading_plugin(app: &mut App) {
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
app
// When entering the state, spawn everything needed for this screen
.add_systems(OnEnter(AppState::Loading), loading_setup)
// While in this state, run the `countdown` system
.add_systems(Update, countdown.run_if(in_state(AppState::Loading)))
// When exiting the state, despawn everything that was spawned for this screen
.add_systems(OnExit(AppState::Loading), despawn_screen::<OnLoadingScreen>);
}
// Tag component used to tag entities added on the splash screen
#[derive(Component)]
struct OnLoadingScreen;
// Newtype to use a `Timer` for this screen as a resource
#[derive(Resource, Deref, DerefMut)]
struct LoadingTimer(Timer);
fn loading_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let icon = asset_server.load("textures/Game Icons/wrench.png");
// Display the logo
commands.spawn((
Node {
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
OnLoadingScreen,
children![(
ImageNode::new(icon),
Node {
// This will set the logo to be 200px wide, and auto adjust its height
width: Val::Px(200.0),
..default()
},
)],
));
// Insert the timer as a resource
commands.insert_resource(LoadingTimer(Timer::from_seconds(1.0, TimerMode::Once)));
}
// Tick the timer, and change state when finished
fn countdown(
mut game_state: ResMut<NextState<AppState>>,
time: Res<Time>,
mut timer: ResMut<LoadingTimer>,
) {
if timer.tick(time.delta()).finished() {
game_state.set(AppState::MainView);
}
}

View file

@ -0,0 +1,475 @@
use bevy::{
app::AppExit,
color::palettes::css::CRIMSON,
ecs::spawn::{SpawnIter, SpawnWith},
prelude::*,
};
use crate::{
despawn_screen,
themes::dark::{self, NORMAL_BUTTON},
DisplayQuality,
AppState,
Volume,
};
#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash, States)]
enum MenuState {
Main,
Settings,
SettingsDisplay,
SettingsVolume,
#[default]
Disabled,
}
pub fn menu_plugin(app: &mut App) {
app
.init_state::<MenuState>()
.add_systems(OnEnter(AppState::Menu), menu_setup)
.add_systems(OnEnter(MenuState::Main), main_menu_setup)
.add_systems(OnExit(MenuState::Main), crate::despawn_screen::<OnMainMenuScreen>)
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
.add_systems(
OnExit(MenuState::Settings),
crate::despawn_screen::<OnSettingsMenuScreen>
)
.add_systems(OnEnter(MenuState::SettingsDisplay), display_settings_menu_setup)
.add_systems(
Update,
(setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)))
)
.add_systems(
OnExit(MenuState::SettingsDisplay),
despawn_screen::<OnDisplaySettingsMenuScreen>
)
.add_systems(OnEnter(MenuState::SettingsVolume), sound_settings_menu_setup)
.add_systems(
Update,
(setting_button::<Volume>.run_if(in_state(MenuState::SettingsVolume)))
)
.add_systems(
OnExit(MenuState::SettingsVolume),
despawn_screen::<OnSoundSettingsMenuScreen>
)
.add_systems(
Update,
(menu_action, button_system).run_if(in_state(AppState::Menu))
)
;
}
#[derive(Component)]
struct OnMainMenuScreen;
#[derive(Component)]
struct OnSettingsMenuScreen;
#[derive(Component)]
struct OnDisplaySettingsMenuScreen;
#[derive(Component)]
struct OnSoundSettingsMenuScreen;
#[derive(Component)]
struct SelectedOption;
#[derive(Component)]
enum MenuButtonAction {
BackToMainView,
BackToMainMenu,
BackToSettings,
Settings,
SettingsDisplay,
SettingsSound,
Quit,
}
fn button_system(
mut interaction_query: Query<(&Interaction, &mut BackgroundColor, Option<&SelectedOption>),(Changed<Interaction>, With<Button>)>
) {
for (interaction, mut background_color, selected) in &mut interaction_query {
*background_color = match (*interaction, selected) {
(Interaction::Pressed, _) | (_, Some(_)) => dark::PRESSED_BUTTON.into(),
(Interaction::Hovered, None) => dark::HOVERED_BUTTON.into(),
(Interaction::None, None) => dark::NORMAL_BUTTON.into(),
}
}
}
fn setting_button<T: Resource + Component + PartialEq + Copy> (
interaction_query: Query<(&Interaction, &T, Entity), (Changed<Interaction>, With<Button>)>,
selected_query: Single<(Entity, &mut BackgroundColor), With<SelectedOption>>,
mut commands: Commands,
mut setting: ResMut<T>
) {
let (previous_button, mut previous_button_color) = selected_query.into_inner();
for (interaction, button_setting, entity) in &interaction_query {
if *interaction == Interaction::Pressed && *setting != *button_setting {
*previous_button_color = NORMAL_BUTTON.into();
commands.entity(previous_button).remove::<SelectedOption>();
commands.entity(entity).insert(SelectedOption);
*setting = *button_setting
}
}
}
fn menu_setup(mut menu_state: ResMut<NextState<MenuState>>) {
menu_state.set(MenuState::Main);
}
fn main_menu_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let button_node = Node {
width: Val::Px(300.0),
height: Val::Px(65.0),
margin: UiRect::all(Val::Px(20.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
};
let button_icon_node = Node {
width: Val::Px(30.0),
position_type: PositionType::Absolute,
left: Val::Px(10.0),
..default()
};
let button_text_font = TextFont {
font_size: 33.0,
..default()
};
let right_icon = asset_server.load("textures/Game Icons/right.png");
let wrench_icon = asset_server.load("textures/Game Icons/wrench.png");
let exit_icon = asset_server.load("textures/Game Icons/exitRight.png");
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
OnMainMenuScreen,
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(dark::BACKGROUND_COLOR.into()),
children![
(
Text::new("Progress Pile"),
TextFont {
font_size: 67.0,
..default()
},
TextColor(dark::TEXT_COLOR),
Node {
margin: UiRect::all(Val::Px(50.0)),
..default()
},
),
(
Button,
button_node.clone(),
BackgroundColor(dark::NORMAL_BUTTON),
MenuButtonAction::BackToMainView,
children![
(ImageNode::new(right_icon), button_icon_node.clone()),
(
Text::new("Back to main window"),
button_text_font.clone(),
TextColor(dark::TEXT_COLOR),
)
]
),
(
Button,
button_node.clone(),
BackgroundColor(dark::NORMAL_BUTTON),
MenuButtonAction::Settings,
children![
(ImageNode::new(wrench_icon), button_icon_node.clone()),
(
Text::new("Settings"),
button_text_font.clone(),
TextColor(dark::TEXT_COLOR),
)
]
),
(
Button,
button_node.clone(),
BackgroundColor(dark::NORMAL_BUTTON),
MenuButtonAction::Quit,
children![
(ImageNode::new(exit_icon), button_icon_node.clone()),
(
Text::new("Quit"),
button_text_font.clone(),
TextColor(dark::TEXT_COLOR),
)
]
),
]
)]
));
}
fn settings_menu_setup(mut commands: Commands) {
let button_node = Node {
width: Val::Px(200.0),
height: Val::Px(65.0),
margin: UiRect::all(Val::Px(20.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
};
let button_text_style = (
TextFont {
font_size: 33.0,
..default()
},
TextColor(dark::TEXT_COLOR),
);
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
OnSettingsMenuScreen,
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
Children::spawn(SpawnIter(
[
(MenuButtonAction::SettingsDisplay, "Display"),
(MenuButtonAction::SettingsSound, "Sound"),
(MenuButtonAction::BackToMainMenu, "Back"),
]
.into_iter()
.map(move |(action, text)| {
(
Button,
button_node.clone(),
BackgroundColor(NORMAL_BUTTON),
action,
children![(Text::new(text), button_text_style.clone())],
)
})
))
)],
));
}
fn display_settings_menu_setup(mut commands: Commands, display_quality: Res<DisplayQuality>) {
fn button_node() -> Node {
Node {
width: Val::Px(200.0),
height: Val::Px(65.0),
margin: UiRect::all(Val::Px(20.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
}
}
fn button_text_style() -> impl Bundle {
(
TextFont {
font_size: 33.0,
..default()
},
TextColor(dark::TEXT_COLOR),
)
}
let display_quality = *display_quality;
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
OnDisplaySettingsMenuScreen,
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
children![
// Create a new `Node`, this time not setting its `flex_direction`. It will
// use the default value, `FlexDirection::Row`, from left to right.
(
Node {
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
Children::spawn((
// Display a label for the current setting
Spawn((Text::new("Display Quality"), button_text_style())),
SpawnWith(move |parent: &mut ChildSpawner| {
for quality_setting in [
DisplayQuality::Low,
DisplayQuality::Medium,
DisplayQuality::High,
] {
let mut entity = parent.spawn((
Button,
Node {
width: Val::Px(150.0),
height: Val::Px(65.0),
..button_node()
},
BackgroundColor(NORMAL_BUTTON),
quality_setting,
children![(
Text::new(format!("{quality_setting:?}")),
button_text_style(),
)],
));
if display_quality == quality_setting {
entity.insert(SelectedOption);
}
}
})
))
),
// Display the back button to return to the settings screen
(
Button,
button_node(),
BackgroundColor(NORMAL_BUTTON),
MenuButtonAction::BackToSettings,
children![(Text::new("Back"), button_text_style())]
)
]
)],
));
}
fn sound_settings_menu_setup(mut commands: Commands, volume: Res<Volume>) {
let button_node = Node {
width: Val::Px(200.0),
height: Val::Px(65.0),
margin: UiRect::all(Val::Px(20.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
};
let button_text_style = (
TextFont {
font_size: 33.0,
..default()
},
TextColor(dark::TEXT_COLOR),
);
let volume = *volume;
let button_node_clone = button_node.clone();
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
OnSoundSettingsMenuScreen,
children![(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
children![
(
Node {
align_items: AlignItems::Center,
..default()
},
BackgroundColor(CRIMSON.into()),
Children::spawn((
Spawn((Text::new("Volume"), button_text_style.clone())),
SpawnWith(move |parent: &mut ChildSpawner| {
for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] {
let mut entity = parent.spawn((
Button,
Node {
width: Val::Px(30.0),
height: Val::Px(65.0),
..button_node_clone.clone()
},
BackgroundColor(NORMAL_BUTTON),
Volume(volume_setting),
));
if volume == Volume(volume_setting) {
entity.insert(SelectedOption);
}
}
})
))
),
(
Button,
button_node,
BackgroundColor(NORMAL_BUTTON),
MenuButtonAction::BackToSettings,
children![(Text::new("Back"), button_text_style)]
)
]
)],
));
}
fn menu_action(
interaction_query: Query<
(&Interaction, &MenuButtonAction),
(Changed<Interaction>, With<Button>),
>,
mut app_exit_events: EventWriter<AppExit>,
mut menu_state: ResMut<NextState<MenuState>>,
mut game_state: ResMut<NextState<AppState>>,
) {
for (interaction, menu_button_action) in &interaction_query {
if *interaction == Interaction::Pressed {
match menu_button_action {
MenuButtonAction::Quit => {
app_exit_events.write(AppExit::Success);
}
MenuButtonAction::BackToMainView => {
game_state.set(AppState::MainView);
menu_state.set(MenuState::Disabled);
}
MenuButtonAction::Settings => menu_state.set(MenuState::Settings),
MenuButtonAction::SettingsDisplay => {
menu_state.set(MenuState::SettingsDisplay);
}
MenuButtonAction::SettingsSound => {
menu_state.set(MenuState::SettingsVolume);
}
MenuButtonAction::BackToMainMenu => menu_state.set(MenuState::Main),
MenuButtonAction::BackToSettings => {
menu_state.set(MenuState::Settings);
}
}
}
}
}

View file

@ -0,0 +1,113 @@
use bevy::{
app::AppExit,
color::palettes::css::CRIMSON,
ecs::spawn::{SpawnIter, SpawnWith},
prelude::*,
};
use crate::{
despawn_screen,
themes::dark::{self, NORMAL_BUTTON},
DisplayQuality,
AppState,
Volume,
};
pub fn main_view_plugin(app: &mut App) {
app
.add_systems(OnEnter(AppState::MainView), main_view_setup)
.add_systems(OnExit(AppState::MainView), despawn_screen::<OnMainViewScreen>)
.add_systems(
Update,
(menu_action, button_system).run_if(in_state(AppState::MainView))
)
;
}
#[derive(Component)]
struct OnMainViewScreen;
#[derive(Component)]
struct SelectedOption;
#[derive(Component)]
enum MainViewButtonAction {
OpenMainMenu,
}
fn button_system(
mut interaction_query: Query<(&Interaction, &mut BackgroundColor, Option<&SelectedOption>),(Changed<Interaction>, With<Button>)>
) {
for (interaction, mut background_color, selected) in &mut interaction_query {
*background_color = match (*interaction, selected) {
(Interaction::Pressed, _) | (_, Some(_)) => dark::PRESSED_BUTTON.into(),
(Interaction::Hovered, None) => dark::HOVERED_BUTTON.into(),
(Interaction::None, None) => dark::NORMAL_BUTTON.into(),
}
}
}
fn main_view_setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let header_node = Node {
width: Val::Percent(100.0),
height: Val::Px(20.0),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
};
let body_node = Node{
width: Val::Percent(100.0),
height: Val::Auto,
..default()
};
let wrench_icon = asset_server.load("textures/Game Icons/wrench.png");
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
OnMainViewScreen,
children![(
header_node,
children![(
Button,
Node{
width: Val::Px(30.0),
..default()
},
MainViewButtonAction::OpenMainMenu,
children![(
ImageNode::new(wrench_icon),
Node::default()
)
]
)]
)]
)
);
}
fn menu_action(
interaction_query: Query<
(&Interaction, &MainViewButtonAction),
(Changed<Interaction>, With<Button>),
>,
mut app_exit_events: EventWriter<AppExit>,
mut game_state: ResMut<NextState<AppState>>,
) {
for (interaction, menu_button_action) in &interaction_query {
if *interaction == Interaction::Pressed {
match menu_button_action {
MainViewButtonAction::OpenMainMenu => {
game_state.set(AppState::Menu);
}
}
}
}
}

View file

@ -0,0 +1,3 @@
pub mod loading;
pub mod main_menu;
pub mod main_view;

View file

@ -0,0 +1,7 @@
use bevy::prelude::*;
pub const TEXT_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
pub const BACKGROUND_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
pub const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
pub const HOVERED_BUTTON: Color= Color::srgb(0.25, 0.25, 0.25);
pub const PRESSED_BUTTON: Color = Color::srgb(0.5,0.5, 0.5);

View file

@ -0,0 +1,4 @@
use bevy::prelude::*;
const TEXT_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);

View file

@ -0,0 +1,12 @@
use bevy::prelude::*;
pub mod dark;
pub mod light;
#[derive(Resource, Debug, Component, Default, PartialEq, Eq, Clone, Copy)]
pub enum UiTheme {
Light,
Dark,
#[default]
Default,
}

View file

@ -1,15 +0,0 @@
[package]
name = "progress-pile-cli"
version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
progress-pile-client = {workspace = true}
chrono = {workspace = true}
chrono-tz.workspace = true
clap = {workspace = true, features = ["derive"]}
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true

View file

@ -1,73 +0,0 @@
use clap::{Args, Subcommand};
use chrono_tz::Tz;
use crate::error::Error;
use progress_pile_client::auth::try_login;
use progress_pile_client::config::{
ClientConfig, ClientRemoteStorageConfig, ClientStorageConfig, Config, GlobalConfig, PartialGlobalConfig
};
#[derive(Args, Clone, Debug)]
pub struct InitArgs {
#[command(subcommand)]
pub command: InitCommand,
}
impl InitArgs {
pub async fn run(self) -> Result<(), Error> {
match self.command {
InitCommand::Local(x) => x.run().await,
InitCommand::Remote(x) => x.run().await,
}
}
}
#[derive(Clone, Debug, Subcommand)]
enum InitCommand {
Local(InitLocalArgs),
Remote(InitRemoteArgs),
}
#[derive(Args, Clone, Debug)]
pub struct InitLocalArgs {
#[arg(short, long)]
pub time_zone: Option<Tz>,
#[arg(short, long)]
pub force: bool
}
impl InitLocalArgs {
pub async fn run(self) -> Result<(), Error> {
unimplemented!()
}
}
#[derive(Args, Clone, Debug)]
pub struct InitRemoteArgs {
pub endpoint: String,
#[arg(short, long)]
pub user_name: String,
#[arg(short, long)]
pub password: String,
#[arg(short, long)]
pub time_zone: Option<Tz>,
#[arg(short, long)]
pub force: bool,
}
impl InitRemoteArgs {
pub async fn run(self) -> Result<(), Error> {
let token: String = try_login(&self.user_name, &self.password, &self.endpoint)?;
let config: Config = Config{
global: GlobalConfig {
time_zone: self.time_zone.clone()
},
client: ClientConfig {
storage: ClientStorageConfig::Remote(ClientRemoteStorageConfig {
endpoint: self.endpoint,
access_key: token,
}),
}
};
config.write_to_default_toml().await
}
}

View file

@ -1,42 +0,0 @@
//mod label;
mod init;
mod record;
mod user;
pub use progress_pile_client::error;
//use label::LabelArgs;
use record::{RecordArgs,RecordAddArgs};
use error::Error;
use init::InitArgs;
use clap::{Args, CommandFactory, Parser, Subcommand};
use std::ffi::OsString;
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version=true)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Clone, Debug, Subcommand)]
enum Command {
Init(InitArgs),
Record(RecordArgs),
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let cli = Cli::parse();
match cli.command {
//Some(Commands::Add(x)) => x.run(),
Command::Init(x) => x.run().await,
//Some(Commands::Label(x)) => x.run(),
Command::Record(x) => x.run(),
}
}

View file

@ -1,59 +0,0 @@
use chrono::prelude::*;
use clap::{Args, Subcommand};
use crate::error::Error;
use std::str::FromStr;
#[derive(Args, Clone, Debug)]
pub struct AchievementArgValues {
pub label: String,
pub value: i8,
}
impl FromStr for AchievementArgValues {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let strvec: Vec<&str> = s.split(':').collect();
Ok(AchievementArgValues{
label: strvec.get(0).unwrap().to_string(),
value: strvec.get(1).unwrap().parse()?
})
}
}
#[derive(Args, Clone, Debug)]
pub struct RecordAddArgs {
#[arg(short, long)]
pub comment: Option<String>,
#[arg(short, long)]
pub time: Option<DateTime<Utc>>,
//pub achievements: Vec<String>,
}
impl RecordAddArgs {
pub fn run(self) -> Result<(), Error> {
unimplemented!();
}
}
#[derive(Clone, Debug, Subcommand)]
pub enum RecordCommand {
Add(RecordAddArgs),
}
#[derive(Args, Clone, Debug)]
pub struct RecordArgs {
#[command(subcommand)]
command: RecordCommand,
}
impl RecordArgs {
pub fn run(self) -> Result<(), Error> {
match self.command {
RecordCommand::Add(x) => x.run(),
}
}
}

View file

@ -4,23 +4,23 @@ version = "0.1.0"
edition = "2024"
[features]
default = ["clap"]
clap = ["dep:clap"]
default = ["desktop"]
desktop = ["dep:clap", "progress-pile-core/desktop", "dep:progress-pile-migration-client", "dep:sea-orm", "tokio/io-util", "tokio/fs"]
web = ["uuid/js"]
[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
progress-pile-migration-client = { workspace = true, optional = true}
sea-orm = { workspace = true, optional = true }
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true
uuid.workspace = true
[dev-dependencies]
[target.'cfg(not(target_family="wasm"))'.dev-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,27 @@
use clap::{Args, Subcommand};
#[derive(Args, Clone, Debug)]
pub struct CategoryArgs {
#[command(subcommand)]
pub command: CategoryCommand,
}
impl CategoryArgs {
pub async fn run(self){
self.command.run().await
}
}
#[derive(Clone, Debug, Subcommand)]
enum CategoryCommand {
List,
Add,
Modify,
}
impl CategoryCommand {
async fn run(self) {
todo!()
}
}

View file

@ -0,0 +1,26 @@
use clap::{Args, Subcommand};
#[derive(Args, Clone, Debug)]
pub struct ConfigArgs {
#[command(subcommand)]
pub command: ConfigCommand,
}
impl ConfigArgs {
pub async fn run(self){
self.command.run().await
}
}
#[derive(Clone, Debug, Subcommand)]
enum ConfigCommand {
Show,
Modify,
}
impl ConfigCommand {
async fn run(self) {
todo!()
}
}

View file

@ -0,0 +1,16 @@
use clap::{Args, Subcommand};
#[derive(Args, Clone, Debug)]
pub struct DefaultArgs {
#[arg(short, long)]
pub note: String,
pub category: String,
pub quantity: i32,
}
impl DefaultArgs {
pub async fn run (self) {
todo!()
}
}

View file

@ -0,0 +1,18 @@
use clap::{Args, Subcommand};
use chrono_tz::Tz;
use crate::error::Error;
#[derive(Args, Clone, Debug)]
pub struct LoginArgs {
pub endpoint: String,
#[arg(short, long)]
pub user_name: String,
#[arg(short, long)]
pub password: String,
}
impl LoginArgs {
pub async fn run(self){
todo!()
}
}

View file

@ -0,0 +1,50 @@
//mod label;
mod category;
mod config;
mod default;
mod login;
mod user;
use category::CategoryArgs;
use config::ConfigArgs;
use default::DefaultArgs;
use login::LoginArgs;
use user::UserArgs;
use clap::{Args, CommandFactory, Parser, Subcommand};
use std::ffi::OsString;
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version=true)]
pub struct Cli {
#[command(subcommand)]
command: Option<Command>,
#[command(flatten)]
args: Option<DefaultArgs>
}
impl Cli {
pub async fn run(self) {
if let Some(x) = self.command {
x.run().await
} else if let Some(x) = self.args {
x.run().await
}
}
}
#[derive(Clone, Debug, Subcommand)]
enum Command {
Category(CategoryArgs),
Config(ConfigArgs),
Login(LoginArgs),
User(UserArgs),
}
impl Command {
pub async fn run(self) {
todo!()
}
}

View file

@ -1,6 +1,5 @@
use clap::{Args, Subcommand};
use crate::error::Error;
use progress_pile_client::config::Config;
#[derive(Args, Clone, Debug)]
@ -11,7 +10,6 @@ pub struct UserArgs {
impl UserArgs {
pub async fn run(self) -> Result<(), Error> {
Config::read_from_default_toml().await?.set_global();
match self.command {
UserCommand::Add(x) => x.add().await,
UserCommand::List => todo!(),

View file

@ -1,9 +1,8 @@
mod storage;
mod remote;
use std::{fmt::{Display, Formatter}, path::{Path, PathBuf}, str::FromStr};
pub use progress_pile_core::config::*;
pub use storage::*;
pub use remote::*;
use crate::error::Error;
use serde::{
@ -11,26 +10,24 @@ use serde::{
Serialize
};
use tokio::{fs::{self, File}, io::AsyncWriteExt, sync::OnceCell};
#[cfg(feature="desktop")]
use tokio::{fs::{self, File}, io::AsyncWriteExt};
pub static DEFAULT_CONFIG_FILE_NAME: &str = "dpts_client.toml";
pub static DEFAULT_CONFIG_DIR_NAME: &str = "dpts";
pub static CLIENT_CONFIG: OnceCell<ClientConfig> = OnceCell::const_new();
pub fn get_default_config_file_path() -> Result<PathBuf, Error> {
let config_dir = dirs::config_dir().ok_or(Error::DefaultConfigDir)?;
Ok(config_dir.join(DEFAULT_CONFIG_DIR_NAME).join(DEFAULT_CONFIG_FILE_NAME))
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Config {
pub client: ClientConfig,
pub global: GlobalConfig,
pub struct ClientConfig {
pub remote: ClientRemoteConfig,
}
impl Config {
#[cfg(feature="desktop")]
impl ClientConfig {
pub async fn read_from_default_toml() -> Result<Self, Error> {
Ok(Self::read_from_toml(&get_default_config_file_path()?).await?)
}
@ -53,90 +50,9 @@ impl Config {
pub fn to_toml(&self) -> Result<String, Error> {
Ok(toml::to_string(self)?)
}
pub fn set_global(self) -> Result<(), Error> {
CLIENT_CONFIG.set(self.client)?;
GLOBAL_CONFIG.set(self.global)?;
Ok(())
}
pub fn from_global() -> Result<Self, Error> {
Ok(Self {
client: CLIENT_CONFIG.get().ok_or(Error::UninitializedOnceCell("CLIENT_CONFIG".to_string()))?.clone(),
global: GLOBAL_CONFIG.get().ok_or(Error::UninitializedOnceCell("GLOBAL_CONFIG".to_string()))?.clone(),
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ClientConfig {
pub storage: ClientStorageConfig,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono_tz::UTC;
use tokio::sync::OnceCell;
const LOCAL_STORAGE_CONFIG_TOML: &str = r#"[client]
storage = "local"
[global]
"#;
static LOCAL_STORAGE_CONFIG_STRUCT: OnceCell<Config> = OnceCell::const_new();
async fn get_local_storage_client_config_struct() -> &'static Config {
LOCAL_STORAGE_CONFIG_STRUCT.get_or_init(|| async {
Config{
client: ClientConfig{
storage: ClientStorageConfig::Local,
},
global: GlobalConfig { time_zone: None },
}
}).await
}
#[tokio::test]
async fn deserialize_local_storage_client_config() {
let config: Config = Config::from_toml(LOCAL_STORAGE_CONFIG_TOML).unwrap();
assert_eq!(&config, get_local_storage_client_config_struct().await);
}
#[tokio::test]
async fn serialize_local_storage_client_config() {
assert_eq!(LOCAL_STORAGE_CONFIG_TOML, get_local_storage_client_config_struct().await.to_toml().unwrap());
}
const REMOTE_STORAGE_CONFIG_TOML: &str = r#"[client.storage.remote]
endpoint = "https://example.com"
access_key = "test"
[global]
time_zone = "UTC"
"#;
static REMOTE_STORAGE_CONFIG_STRUCT: OnceCell<Config> = OnceCell::const_new();
async fn get_remote_storage_client_config_struct() -> &'static Config {
REMOTE_STORAGE_CONFIG_STRUCT.get_or_init(|| async {
Config{
client: ClientConfig {
storage: ClientStorageConfig::Remote(ClientRemoteStorageConfig {
endpoint: "https://example.com".to_string(),
access_key: "test".to_string(),
})
},
global: GlobalConfig { time_zone: Some(UTC) }
}
}).await
}
#[tokio::test]
async fn deserialize_remote_storage_client_config() { let config: Config = Config::from_toml(REMOTE_STORAGE_CONFIG_TOML).unwrap();
assert_eq!(&config, get_remote_storage_client_config_struct().await);
}
#[tokio::test]
async fn serialize_remote_storage_client_config() {
assert_eq!(REMOTE_STORAGE_CONFIG_TOML, get_remote_storage_client_config_struct().await.to_toml().unwrap());
}
}

View file

@ -10,8 +10,8 @@ use std::{
};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ClientRemoteStorageConfig {
#[serde(rename_all = "snake_case")]
pub struct ClientRemoteConfig {
pub endpoint: String,
pub access_key: String,
}

View file

@ -24,7 +24,6 @@ mod tests {
use chrono::Local;
use progress_pile_core::global::GlobalDatabase;
use sea_orm::{entity::*, ConnectOptions, Database, DatabaseConnection};
use progress_pile_migration::{ClientMigrator, MigratorTrait};
use uuid::Uuid;
use crate::error::Error;
use crate::global::GLOBAL;

View file

@ -1,6 +1,5 @@
use core::time;
use async_graphql::*;
use chrono::Local;
use sea_orm::entity::{
*,

View file

@ -1,4 +1,3 @@
use async_graphql::*;
use chrono::Local;
use sea_orm::entity::{
*,

View file

@ -1,4 +1,4 @@
use progress_pile_migration::{ClientMigrator, MigratorTrait};
use progress_pile_migration_client::{Migrator, MigratorTrait};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::error::Error;
use tokio::sync::OnceCell;
@ -19,7 +19,7 @@ impl GlobalDatabase for Global {
{
Ok(self.database.get_or_try_init(|| async {
let db = Database::connect(options).await?;
ClientMigrator::up(&db, None).await?;
Migrator::up(&db, None).await?;
Ok::<DatabaseConnection, Error>(db)
}).await?)
}

View file

@ -1,15 +1,18 @@
use crate::config::ClientConfig;
#[cfg(feature="desktop")]
use sea_orm::DatabaseConnection;
use tokio::sync::OnceCell;
#[cfg(feature="desktop")]
mod database;
pub static GLOBAL: Global = Global{
config: OnceCell::const_new(),
#[cfg(feature="desktop")]
database: OnceCell::const_new(),
};
pub struct Global {
config: OnceCell<ClientConfig>,
#[cfg(feature="desktop")]
database: OnceCell<DatabaseConnection>,
}

View file

@ -1,11 +1,12 @@
pub mod auth;
#[cfg(feature="desktop")]
pub mod cli;
pub mod config;
#[cfg(feature="desktop")]
pub mod entity;
pub mod global;
pub use progress_pile_core::{
error,
};
pub use progress_pile_core::error;

View file

@ -0,0 +1,8 @@
use clap::Parser;
use progress_pile_client::cli::Cli;
#[tokio::main]
async fn main() {
let cli = Cli::parse();
cli.run().await
}

View file

@ -6,14 +6,12 @@ license.workspace = true
repository.workspace = true
[features]
default = ["clap",]
clap = ["dep:clap"]
postgres = ["sea-orm/sqlx-postgres", "sea-orm/sqlx-postgres"]
server = ["clap", "postgres"]
default = ["desktop"]
desktop = ["dep:clap", "dep:sea-orm"]
wasm = []
[dependencies]
async-graphql.workspace = true
axum = "0.8"
chrono = {workspace = true}
chrono-tz.workspace = true
clap = { workspace = true, optional = true }
@ -22,9 +20,9 @@ cynic = "3.10.0"
dotenv = {workspace = true}
iana-time-zone = "0.1.63"
log = "0.4.27"
sea-orm.workspace = true
sea-orm-migration.workspace = true
sea-orm = { workspace = true, optional = true }
serde.workspace = true
thiserror.workspace = true
tokio.workspace = true
toml.workspace = true

View file

@ -1,32 +0,0 @@
use chrono_tz::Tz;
#[cfg(feature="clap")]
use clap::Args;
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use crate::error::Error;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct GlobalConfig {
pub time_zone: Option<Tz>,
}
impl TryFrom<PartialGlobalConfig> for GlobalConfig{
type Error = Error;
fn try_from(p: PartialGlobalConfig) -> Result<GlobalConfig, Self::Error> {
Ok(GlobalConfig{
time_zone: p.time_zone,
})
}
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature="clap", derive(Args))]
pub struct PartialGlobalConfig {
#[cfg_attr(feature="clap", arg(short, long))]
pub time_zone: Option<Tz>,
}
pub static GLOBAL_CONFIG: OnceCell<GlobalConfig> = OnceCell::const_new();

View file

@ -1,14 +0,0 @@
mod database;
mod global;
pub use database::{
DatabaseConfig,
PartialDatabaseConfig,
DATABASE_CONFIG,
};
pub use global::{
GlobalConfig,
PartialGlobalConfig,
GLOBAL_CONFIG,
};

View file

@ -20,6 +20,7 @@ pub enum Error {
InitializingOnceCell(String),
#[error("Once cell is already Initialized: {0}")]
AlreadyInitializedOnceCell(String),
#[cfg(feature="desktop")]
#[error("DB Error: {0}")]
Db(#[from]sea_orm::DbErr),
}

View file

@ -1,10 +1,11 @@
use chrono_tz::Tz;
use crate::{config::DatabaseConfig, error::Error};
use crate::error::Error;
pub trait GlobalConfig<T> {
fn get_config(&self) -> Option<T>;
fn get_or_try_init_config(&self) -> Result<T, Error>;
fn get_database_config(&self) -> Option<DatabaseConfig>;
fn get_or_try_init_config_from_file(&self) -> Result<T, Error>;
fn get_or_try_init_config_from_str(&self) -> Result<T, Error>;
fn get_time_zone(&self) -> Option<Tz>;
}

View file

@ -1,17 +1,9 @@
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::error::Error;
use sea_orm::{ConnectOptions, Database, DatabaseConnection, DbErr};
use sea_orm_migration::MigratorTrait;
use tokio::sync::OnceCell;
pub trait GlobalDatabase {
fn get_database(&self) -> Option<&DatabaseConnection>;
async fn get_or_try_init_database(&self) -> Result<&DatabaseConnection, Error>;
async fn get_or_try_init_database_with_connect_options<T>(&self, options: T) -> Result<&DatabaseConnection, Error> where
T: Into<ConnectOptions>;
}
#[cfg(test)]
mod tests {
use super::*;
T: Into<ConnectOptions>;
}

View file

@ -1,5 +1,8 @@
mod config;
#[cfg(feature="desktop")]
mod database;
pub use config::GlobalConfig;
#[cfg(feature="desktop")]
pub use database::GlobalDatabase;

View file

@ -1,4 +1,3 @@
pub mod config;
pub mod csv;
pub mod entity;
pub mod error;

View file

@ -0,0 +1,19 @@
[package]
name = "progress-pile-migration-client"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
progress-pile-migration-core.workspace = true
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
]

View file

@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View file

@ -0,0 +1,63 @@
use progress_pile_migration_core::m20220101_000001_create_table::{
TableMigration,
ProgressCategory as DefaultProgressCategory,
ProgressEntry as DefaultProgressEntry,
PK_PROGRESS_CATEGORY,
PK_PROGRESS_ENTITY,
};
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressCategory::up(manager).await?;
ProgressEntry::up(manager).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressEntry::down(manager).await?;
ProgressCategory::down(manager).await?;
Ok(())
}
}
#[derive(DeriveIden)]
pub enum ProgressCategory{}
impl TableMigration for ProgressCategory {
fn table_create_statement() -> TableCreateStatement{
let mut tcs = DefaultProgressCategory::table_create_statement();
tcs.primary_key(Index::create().name(PK_PROGRESS_CATEGORY).col(DefaultProgressCategory::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
DefaultProgressCategory::index_create_statements()
}
fn table_drop_statement() -> TableDropStatement{
DefaultProgressCategory::table_drop_statement()
}
}
#[derive(DeriveIden)]
pub enum ProgressEntry {}
impl TableMigration for ProgressEntry {
fn table_create_statement() -> TableCreateStatement{
let mut tcs: TableCreateStatement = DefaultProgressEntry::table_create_statement();
tcs.primary_key(Index::create().name(PK_PROGRESS_ENTITY).col(DefaultProgressEntry::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
DefaultProgressEntry::index_create_statements()
}
fn table_drop_statement() -> TableDropStatement {
DefaultProgressEntry::table_drop_statement()
}
}

View file

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(progress_pile_migration_client::Migrator).await;
}

View file

@ -0,0 +1,9 @@
[package]
name = "progress-pile-migration-core"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
sea-orm-migration = "1.1.0"

View file

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View file

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

View file

@ -0,0 +1,145 @@
use sea_orm_migration::{prelude::*, schema::*};
pub trait TableMigration {
async fn up<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.create_table(Self::table_create_statement()).await?;
for statement in Self::index_create_statements().into_iter() {
manager.create_index(statement).await?
}
Ok(())
}
async fn down<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.drop_table(Self::table_drop_statement()).await?;
Ok(())
}
fn table_create_statement() -> TableCreateStatement;
fn index_create_statements() -> Vec<IndexCreateStatement>;
fn table_drop_statement() -> TableDropStatement;
}
#[derive(DeriveIden)]
pub enum ProgressCategory {
Table,
Id,
CreatedAt,
UpdatedAt,
DeletedAt,
Name,
}
static IDX_PROGRESS_CATEGORY_NAME: &str = "idx_progress_category_name";
static IDX_PROGRESS_CATEGORY_CREATED_AT: &str = "idx_progress_category_created_at";
static IDX_PROGRESS_CATEGORY_UPDATED_AT: &str = "idx_progress_category_updated_at";
static IDX_PROGRESS_CATEGORY_DELETED_AT: &str = "idx_progress_category_deleted_at";
pub static PK_PROGRESS_CATEGORY: &str = "pk_progress_category";
impl TableMigration for ProgressCategory {
fn table_create_statement() -> TableCreateStatement {
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))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_PROGRESS_CATEGORY_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_DELETED_AT)
.table(Self::Table)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_NAME)
.table(Self::Table)
.col(Self::Name)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum ProgressEntry {
Table,
Id,
ProgressCategoryId,
CreatedAt,
UpdatedAt,
DeletedAt,
ProgressedAt,
Quantity,
Note,
}
static IDX_PROGRESS_ENTITY_CREATED_AT: &str = "idx_progress_entity_created_at";
static IDX_PROGRESS_ENTITY_UPDATED_AT: &str = "idx_progress_entity_updated_at";
static IDX_PROGRESS_ENTITY_DELETED_AT: &str = "idx_progress_entity_deleted_at";
static IDX_PROGRESS_ENTITY_PROGRESSED_AT: &str = "idx_progress_entity_progressed_at";
static FK_PROGRESS_ENTITY_PROGRESS_CATEGORY: &str = "fk_progress_entity_progress_category";
pub static PK_PROGRESS_ENTITY: &str = "pk_progress_entity";
impl TableMigration for ProgressEntry {
fn table_create_statement() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(uuid(Self::Id))
.col(uuid(Self::ProgressCategoryId))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.col(timestamp_with_time_zone(Self::ProgressedAt))
.col(integer(Self::Quantity))
.col(string(Self::Note))
.foreign_key(ForeignKey::create()
.name(FK_PROGRESS_ENTITY_PROGRESS_CATEGORY)
.from(Self::Table, Self::ProgressCategoryId)
.to(ProgressCategory::Table, ProgressCategory::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_PROGRESS_ENTITY_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_DELETED_AT)
.table(Self::Table)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_PROGRESSED_AT)
.table(Self::Table)
.col(Self::ProgressedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}

View file

@ -0,0 +1,19 @@
[package]
name = "progress-pile-migration-server"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
progress-pile-migration-core.workspace = true
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
]

View file

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View file

@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View file

@ -0,0 +1,289 @@
use progress_pile_migration_core::m20220101_000001_create_table::{
TableMigration,
ProgressCategory as DefaultProgressCategory,
ProgressEntry as DefaultProgressEntry,
PK_PROGRESS_CATEGORY,
PK_PROGRESS_ENTITY,
};
use sea_orm_migration::{prelude::*, schema::*};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
User::up(manager).await?;
AccessToken::up(manager).await?;
ProgressCategory::up(manager).await?;
ProgressEntry::up(manager).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressEntry::down(manager).await?;
ProgressCategory::down(manager).await?;
AccessToken::down(manager).await?;
User::down(manager).await
}
}
#[derive(DeriveIden)]
pub enum User {
Table,
Id,
CreatedAt,
UpdatedAt,
DeletedAt,
LoginName,
PasswordHash,
}
static IDX_USER_LOGIN_NAME: &str = "idx_user_login_name";
static IDX_USER_CREATED_AT: &str = "idx_user_created_at";
static IDX_USER_UPDATED_AT: &str = "idx_user_updated_at";
static IDX_USER_DELETED_AT: &str = "idx_user_deleted_at";
impl TableMigration for User {
fn table_create_statement() -> TableCreateStatement{
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.col(string_uniq(Self::LoginName))
.col(string(Self::PasswordHash))
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_USER_LOGIN_NAME)
.table(Self::Table)
.col(Self::LoginName)
.to_owned(),
Index::create().name(IDX_USER_CREATED_AT)
.table(Self::Table)
.col((Self::CreatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_UPDATED_AT)
.table(Self::Table)
.col((Self::UpdatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_DELETED_AT)
.table(Self::Table)
.col((Self::DeletedAt, IndexOrder::Desc))
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum AccessToken{
Table,
Id,
UserId,
CreatedAt,
UpdatedAt,
ExpiredAt,
TokenValue,
Note,
}
static IDX_ACCESS_TOKEN_TOKEN_VALUE: &str = "idx_access_token_token_value";
static IDX_ACCESS_TOKEN_CREATED_AT: &str = "idx_access_token_created_at";
static IDX_ACCESS_TOKEN_UPDATED_AT: &str = "idx_access_token_updated_at";
static IDX_ACCESS_TOKEN_EXPIRED_AT: &str = "idx_access_token_expired_at";
static IDX_ACCESS_TOKEN_USER_ID_CREATED_AT: &str = "idx_access_token_user_id_created_at";
static IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT: &str = "idx_access_token_user_id_updated_at";
static IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT: &str = "idx_access_token_user_id_expired_at";
static FK_ACCESS_TOKEN_USER: &str = "fk_access_token_user";
impl TableMigration for AccessToken {
fn table_create_statement() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(integer(Self::UserId))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::ExpiredAt))
.col(string(Self::TokenValue))
.col(string(Self::Note))
.foreign_key(ForeignKey::create()
.name(FK_ACCESS_TOKEN_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_ACCESS_TOKEN_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_EXPIRED_AT)
.table(Self::Table)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_TOKEN_VALUE)
.table(Self::Table)
.col(Self::TokenValue)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum ProgressCategory {
Table,
UserId,
}
static IDX_PROGRESS_CATEGORY_USER_ID_NAME: &str = "idx_progress_category_user_id_name";
static IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT: &str = "idx_progress_category_user_id_created_at";
static IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT: &str = "idx_progress_category_user_id_updated_at";
static IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT: &str = "idx_progress_category_user_id_deleted_at";
static FK_PROGRESS_CATEGORY_USER: &str = "fk_progress_category_user";
impl TableMigration for ProgressCategory {
fn table_create_statement() -> TableCreateStatement{
let mut tcs = DefaultProgressCategory::table_create_statement();
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));
tcs.primary_key(Index::create().name(PK_PROGRESS_CATEGORY).col(Self::UserId).col(DefaultProgressCategory::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
[DefaultProgressCategory::index_create_statements(), vec![
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_NAME)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::Name)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressCategory::UpdatedAt)
.to_owned(),
]].concat()
}
fn table_drop_statement() -> TableDropStatement {
DefaultProgressCategory::table_drop_statement()
}
}
#[derive(DeriveIden)]
pub enum ProgressEntry {
Table,
UserId,
}
static IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT: &str = "idx_progress_entity_user_id_created_at";
static IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT: &str = "idx_progress_entity_user_id_updated_at";
static IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT: &str = "idx_progress_entity_user_id_deleted_at";
static IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT: &str = "idx_progress_entity_user_id_progressed_at";
static FK_PROGRESS_ENTITY_PROGRESS_CATEGORY: &str = "fk_progress_entity_progress_category";
static FK_PROGRESS_ENTITY_USER: &str = "fk_progress_entity_user";
impl TableMigration for ProgressEntry {
fn table_create_statement() -> TableCreateStatement{
let mut tcs: TableCreateStatement = DefaultProgressEntry::table_create_statement();
tcs.col(integer(Self::UserId));
tcs.foreign_key(ForeignKey::create()
.name(FK_PROGRESS_ENTITY_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
);
tcs.primary_key(Index::create().name(PK_PROGRESS_ENTITY).col(Self::UserId).col(DefaultProgressEntry::Id));
tcs
}
fn index_create_statements() -> Vec<IndexCreateStatement> {
let mut default = DefaultProgressEntry::index_create_statements();
default.append(&mut vec![
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::ProgressedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(DefaultProgressEntry::UpdatedAt)
.to_owned(),
]);
default
}
fn table_drop_statement() -> TableDropStatement {
DefaultProgressEntry::table_drop_statement()
}
}

View file

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(progress_pile_migration_server::Migrator).await;
}

View file

@ -1,20 +0,0 @@
[package]
name = "progress-pile-migration"
version = "0.1.0"
edition = "2021"
publish = false
[features]
default = ["client", "server"]
client = []
server = ["sea-orm-migration/sqlx-postgres"]
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "1.1.0"
features = [
"runtime-tokio-rustls",
"sqlx-sqlite",
]

View file

@ -1,52 +0,0 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
#[cfg(feature="client")]
pub struct ClientMigrator;
#[cfg(feature="client")]
#[async_trait::async_trait]
impl MigratorTrait for ClientMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_create_table::ClientMigration),
]
}
}
#[cfg(feature="server")]
pub struct ServerMigrator;
#[cfg(feature="server")]
#[async_trait::async_trait]
impl MigratorTrait for ServerMigrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_create_table::ServerMigration),
]
}
}
#[cfg(test)]
mod tests {
use sea_orm_migration::sea_orm::{ConnectOptions, Database};
use super::*;
#[cfg(feature="client")]
#[async_std::test]
async fn client_migration() {
let db = Database::connect(ConnectOptions::new("sqlite::memory:")).await.unwrap();
ClientMigrator::up(&db, None).await.unwrap();
ClientMigrator::down(&db, None).await.unwrap();
}
#[cfg(feature="server")]
#[async_std::test]
async fn server_migration() {
let db = Database::connect(ConnectOptions::new("sqlite::memory:")).await.unwrap();
ServerMigrator::up(&db, None).await.unwrap();
ServerMigrator::down(&db, None).await.unwrap();
}
}

View file

@ -1,532 +0,0 @@
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> {
ProgressCategory::up_client(manager).await?;
ProgressEntry::up_client(manager).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressEntry::down_client(manager).await?;
ProgressCategory::down_client(manager).await?;
Ok(())
}
}
#[cfg(feature="server")]
#[derive(DeriveMigrationName)]
pub struct ServerMigration;
#[cfg(feature="server")]
#[async_trait::async_trait]
impl MigrationTrait for ServerMigration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
User::up_server(manager).await?;
AccessToken::up_server(manager).await?;
ProgressCategory::up_server(manager).await?;
ProgressEntry::up_server(manager).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
ProgressEntry::down_server(manager).await?;
ProgressCategory::down_server(manager).await?;
AccessToken::down_server(manager).await?;
User::down_server(manager).await
}
}
pub trait MigrationTableDefault {
fn table_create_statement_default() -> TableCreateStatement;
fn index_create_statements_default() -> Vec<IndexCreateStatement>;
fn table_drop_statement_default() -> TableDropStatement;
}
#[cfg(feature="client")]
#[async_trait::async_trait]
pub trait MigrationTableClient {
async fn up_client<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.create_table(Self::table_create_statement_client()).await?;
for statement in Self::index_create_statements_client().into_iter() {
manager.create_index(statement).await?
}
Ok(())
}
async fn down_client<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.drop_table(Self::table_drop_statement_client()).await?;
Ok(())
}
fn table_create_statement_client() -> TableCreateStatement;
fn index_create_statements_client() -> Vec<IndexCreateStatement>;
fn table_drop_statement_client() -> TableDropStatement;
}
#[cfg(feature="server")]
#[async_trait::async_trait]
pub trait MigrationTableServer {
async fn up_server<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.create_table(Self::table_create_statement_server()).await?;
for statement in Self::index_create_statements_server().into_iter() {
manager.create_index(statement).await?
}
Ok(())
}
async fn down_server<'a>(manager: &'a SchemaManager<'a>) -> Result<(), DbErr> {
manager.drop_table(Self::table_drop_statement_server()).await?;
Ok(())
}
fn table_create_statement_server() -> TableCreateStatement;
fn index_create_statements_server() -> Vec<IndexCreateStatement>;
fn table_drop_statement_server() -> TableDropStatement;
}
#[cfg(feature="server")]
#[derive(DeriveIden)]
pub enum User {
Table,
Id,
CreatedAt,
UpdatedAt,
DeletedAt,
LoginName,
PasswordHash,
}
#[cfg(feature="server")]
static IDX_USER_LOGIN_NAME: &str = "idx_user_login_name";
#[cfg(feature="server")]
static IDX_USER_CREATED_AT: &str = "idx_user_created_at";
#[cfg(feature="server")]
static IDX_USER_UPDATED_AT: &str = "idx_user_updated_at";
#[cfg(feature="server")]
static IDX_USER_DELETED_AT: &str = "idx_user_deleted_at";
#[cfg(feature="server")]
impl MigrationTableServer for User {
fn table_create_statement_server() -> TableCreateStatement{
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.col(string_uniq(Self::LoginName))
.col(string(Self::PasswordHash))
.to_owned()
}
fn index_create_statements_server() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_USER_LOGIN_NAME)
.table(Self::Table)
.col(Self::LoginName)
.to_owned(),
Index::create().name(IDX_USER_CREATED_AT)
.table(Self::Table)
.col((Self::CreatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_UPDATED_AT)
.table(Self::Table)
.col((Self::UpdatedAt, IndexOrder::Desc))
.to_owned(),
Index::create().name(IDX_USER_DELETED_AT)
.table(Self::Table)
.col((Self::DeletedAt, IndexOrder::Desc))
.to_owned(),
]
}
fn table_drop_statement_server() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[cfg(feature="server")]
#[derive(DeriveIden)]
pub enum AccessToken{
Table,
Id,
UserId,
CreatedAt,
UpdatedAt,
ExpiredAt,
TokenValue,
Note,
}
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_TOKEN_VALUE: &str = "idx_access_token_token_value";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_CREATED_AT: &str = "idx_access_token_created_at";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_UPDATED_AT: &str = "idx_access_token_updated_at";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_EXPIRED_AT: &str = "idx_access_token_expired_at";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_USER_ID_CREATED_AT: &str = "idx_access_token_user_id_created_at";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT: &str = "idx_access_token_user_id_updated_at";
#[cfg(feature="server")]
static IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT: &str = "idx_access_token_user_id_expired_at";
#[cfg(feature="server")]
static FK_ACCESS_TOKEN_USER: &str = "fk_access_token_user";
#[cfg(feature="server")]
impl MigrationTableServer for AccessToken {
fn table_create_statement_server() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(pk_auto(Self::Id))
.col(integer(Self::UserId))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::ExpiredAt))
.col(string(Self::TokenValue))
.col(string(Self::Note))
.foreign_key(ForeignKey::create()
.name(FK_ACCESS_TOKEN_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
}
fn index_create_statements_server() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_ACCESS_TOKEN_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_EXPIRED_AT)
.table(Self::Table)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_TOKEN_VALUE)
.table(Self::Table)
.col(Self::TokenValue)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_EXPIRED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::ExpiredAt)
.to_owned(),
Index::create().name(IDX_ACCESS_TOKEN_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement_server() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[derive(DeriveIden)]
pub enum ProgressCategory {
Table,
#[cfg(feature="server")]
UserId,
Id,
CreatedAt,
UpdatedAt,
DeletedAt,
Name,
}
static IDX_PROGRESS_CATEGORY_NAME: &str = "idx_progress_category_name";
static IDX_PROGRESS_CATEGORY_CREATED_AT: &str = "idx_progress_category_created_at";
static IDX_PROGRESS_CATEGORY_UPDATED_AT: &str = "idx_progress_category_updated_at";
static IDX_PROGRESS_CATEGORY_DELETED_AT: &str = "idx_progress_category_deleted_at";
#[cfg(feature="server")]
static IDX_PROGRESS_CATEGORY_USER_ID_NAME: &str = "idx_progress_category_user_id_name";
#[cfg(feature="server")]
static IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT: &str = "idx_progress_category_user_id_created_at";
#[cfg(feature="server")]
static IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT: &str = "idx_progress_category_user_id_updated_at";
#[cfg(feature="server")]
static IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT: &str = "idx_progress_category_user_id_deleted_at";
#[cfg(feature="server")]
static FK_PROGRESS_CATEGORY_USER: &str = "fk_progress_category_user";
static PK_PROGRESS_CATEGORY: &str = "pk_progress_category";
impl MigrationTableDefault for ProgressCategory {
fn table_create_statement_default() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(string(Self::Name))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.to_owned()
}
fn index_create_statements_default() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_PROGRESS_CATEGORY_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_DELETED_AT)
.table(Self::Table)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_NAME)
.table(Self::Table)
.col(Self::Name)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement_default() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[cfg(feature="client")]
impl MigrationTableClient for ProgressCategory {
fn table_create_statement_client() -> TableCreateStatement{
let mut tcs = Self::table_create_statement_default();
tcs.col(pk_uuid(Self::Id));
tcs
}
fn index_create_statements_client() -> Vec<IndexCreateStatement> {
Self::index_create_statements_default()
}
fn table_drop_statement_client() -> TableDropStatement{
Self::table_drop_statement_default()
}
}
#[cfg(feature="server")]
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));
tcs.primary_key(Index::create().name(PK_PROGRESS_CATEGORY).col(Self::UserId).col(Self::Id));
tcs
}
fn index_create_statements_server() -> Vec<IndexCreateStatement> {
[Self::index_create_statements_default(), vec![
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_NAME)
.table(Self::Table)
.col(Self::UserId)
.col(Self::Name)
.to_owned(),
Index::create().name(IDX_PROGRESS_CATEGORY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::UpdatedAt)
.to_owned(),
]].concat()
}
fn table_drop_statement_server() -> TableDropStatement {
Self::table_drop_statement_default()
}
}
#[derive(DeriveIden)]
pub enum ProgressEntry {
Table,
#[cfg(feature="server")]
UserId,
Id,
ProgressCategoryId,
CreatedAt,
UpdatedAt,
DeletedAt,
ProgressedAt,
Quantity,
Note,
}
static IDX_PROGRESS_ENTITY_CREATED_AT: &str = "idx_progress_entity_created_at";
static IDX_PROGRESS_ENTITY_UPDATED_AT: &str = "idx_progress_entity_updated_at";
static IDX_PROGRESS_ENTITY_DELETED_AT: &str = "idx_progress_entity_deleted_at";
static IDX_PROGRESS_ENTITY_PROGRESSED_AT: &str = "idx_progress_entity_progressed_at";
#[cfg(feature="server")]
static IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT: &str = "idx_progress_entity_user_id_created_at";
#[cfg(feature="server")]
static IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT: &str = "idx_progress_entity_user_id_updated_at";
#[cfg(feature="server")]
static IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT: &str = "idx_progress_entity_user_id_deleted_at";
#[cfg(feature="server")]
static IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT: &str = "idx_progress_entity_user_id_progressed_at";
static FK_PROGRESS_ENTITY_PROGRESS_CATEGORY: &str = "fk_progress_entity_progress_category";
#[cfg(feature="server")]
static FK_PROGRESS_ENTITY_USER: &str = "fk_progress_entity_user";
static PK_PROGRESS_ENTITY: &str = "pk_progress_entity";
impl MigrationTableDefault for ProgressEntry {
fn table_create_statement_default() -> TableCreateStatement {
Table::create()
.table(Self::Table)
.if_not_exists()
.col(uuid(Self::Id))
.col(uuid(Self::ProgressCategoryId))
.col(timestamp_with_time_zone(Self::CreatedAt))
.col(timestamp_with_time_zone(Self::UpdatedAt))
.col(timestamp_with_time_zone_null(Self::DeletedAt))
.col(timestamp_with_time_zone(Self::ProgressedAt))
.col(integer(Self::Quantity))
.col(string(Self::Note))
.foreign_key(ForeignKey::create()
.name(FK_PROGRESS_ENTITY_PROGRESS_CATEGORY)
.from(Self::Table, Self::ProgressCategoryId)
.to(ProgressCategory::Table, ProgressCategory::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
)
.to_owned()
}
fn index_create_statements_default() -> Vec<IndexCreateStatement> {
vec![
Index::create().name(IDX_PROGRESS_ENTITY_CREATED_AT)
.table(Self::Table)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_DELETED_AT)
.table(Self::Table)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_PROGRESSED_AT)
.table(Self::Table)
.col(Self::ProgressedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_UPDATED_AT)
.table(Self::Table)
.col(Self::UpdatedAt)
.to_owned(),
]
}
fn table_drop_statement_default() -> TableDropStatement {
Table::drop().table(Self::Table).to_owned()
}
}
#[cfg(feature="client")]
impl MigrationTableClient for ProgressEntry {
fn table_create_statement_client() -> TableCreateStatement{
let mut tcs: TableCreateStatement = Self::table_create_statement_default();
tcs.primary_key(Index::create().name(PK_PROGRESS_ENTITY).col(Self::Id));
tcs
}
fn index_create_statements_client() -> Vec<IndexCreateStatement> {
Self::index_create_statements_default()
}
fn table_drop_statement_client() -> TableDropStatement {
Self::table_drop_statement_default()
}
}
#[cfg(feature = "server")]
impl MigrationTableServer for ProgressEntry {
fn table_create_statement_server() -> TableCreateStatement{
let mut tcs: TableCreateStatement = Self::table_create_statement_default();
tcs.col(integer(Self::UserId));
tcs.foreign_key(ForeignKey::create()
.name(FK_PROGRESS_ENTITY_USER)
.from(Self::Table, Self::UserId)
.to(User::Table, User::Id)
);
tcs.primary_key(Index::create().name(PK_PROGRESS_ENTITY).col(Self::UserId).col(Self::Id));
tcs
}
fn index_create_statements_server() -> Vec<IndexCreateStatement> {
let mut default = Self::index_create_statements_default();
default.append(&mut vec![
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_CREATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::CreatedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_DELETED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::DeletedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_PROGRESSED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::ProgressedAt)
.to_owned(),
Index::create().name(IDX_PROGRESS_ENTITY_USER_ID_UPDATED_AT)
.table(Self::Table)
.col(Self::UserId)
.col(Self::UpdatedAt)
.to_owned(),
]);
default
}
fn table_drop_statement_server() -> TableDropStatement {
Self::table_drop_statement_default()
}
}

View file

@ -10,8 +10,8 @@ argon2 = "0.5.3"
async-graphql.workspace = true
axum = "0.8.4"
clap = {workspace = true, features = ["derive"]}
progress-pile-core = {workspace = true}
progress-pile-migration = { workspace = true, features = ["server"] }
progress-pile-core = {workspace = true, features = ["desktop"]}
progress-pile-migration-server.workspace = true
chrono = {workspace = true}
sea-orm = { workspace = true, features = ["sqlx-postgres"] }
serde.workspace = true

View file

@ -1,29 +0,0 @@
use crate::config::PartialServerConfig;
use clap::Parser;
use progress_pile_core::config::{
PartialDatabaseConfig,
PartialGlobalConfig,
};
use std::{
net::IpAddr,
path::PathBuf,
};
use crate::config::ServerConfig;
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Args {
#[command(flatten)]
pub server: PartialServerConfig,
#[command(flatten)]
pub global: PartialGlobalConfig,
#[command(flatten)]
pub database: PartialDatabaseConfig,
#[arg(short, long)]
pub config_file: Option<PathBuf>,
}

View file

@ -0,0 +1,17 @@
use crate::config::PartialServerConfig;
use clap::Parser;
use std::{
net::IpAddr,
path::PathBuf,
};
use crate::config::ServerConfig;
#[derive(Clone, Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
#[arg(short, long)]
pub config_file: Option<PathBuf>,
}

View file

@ -1,147 +0,0 @@
use crate::error::Error;
use progress_pile_core::config::{
DatabaseConfig, GlobalConfig, PartialDatabaseConfig
};
use serde::Deserialize;
use std::{
default::Default,
str::FromStr,
net::IpAddr,
};
use tokio::sync::OnceCell;
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Config {
global: GlobalConfig,
database: DatabaseConfig,
server: ServerConfig,
}
impl TryFrom<PartialConfig> for Config {
type Error = Error;
fn try_from(p: PartialConfig) -> Result<Config, Self::Error> {
Ok(Config {
global: p.global.ok_or(Error::MissingConfig("global".to_string()))?,
database: p.database.ok_or(Error::MissingConfig("global".to_string()))?,
server: p.server.ok_or(Error::MissingConfig("global".to_string()))?,
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct PartialConfig {
global: Option<GlobalConfig>,
database: Option<DatabaseConfig>,
server: Option<ServerConfig>,
}
pub static SERVER_CONFIG: OnceServerConfig = OnceServerConfig::const_new();
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ServerConfig {
pub listen_ips: Vec<IpAddr>,
pub port: u16,
}
impl ServerConfig {
}
impl TryFrom<PartialServerConfig> for ServerConfig {
type Error = Error;
fn try_from(p: PartialServerConfig) -> Result<ServerConfig, Error>{
Ok(ServerConfig{
listen_ips: p.listen_ips.ok_or(Error::MissingConfig("listen_ips".to_string()))?,
port: p.port.ok_or(Error::MissingConfig("port".to_string()))?,
})
}
}
pub struct OnceServerConfig {
inner: OnceCell<ServerConfig>,
}
impl OnceServerConfig {
const fn const_new() -> Self {
Self {
inner: OnceCell::const_new(),
}
}
pub fn get(&self) -> Option<&ServerConfig> {
self.inner.get()
}
pub async fn get_or_init<F, T>(&self, f: F) -> &ServerConfig where
F: FnOnce() -> T,
T: Future<Output = ServerConfig>
{
self.inner.get_or_init(f).await
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[derive(clap::Args)]
pub struct PartialServerConfig {
#[arg(short, long,)]
pub listen_ips: Option<Vec<IpAddr>>,
#[arg(short, long,)]
pub port: Option<u16>,
}
impl PartialServerConfig {
pub fn try_from_toml(s: &str) -> Result<Self, Error> {
Ok(toml::from_str(s)?)
}
}
impl Default for PartialServerConfig {
fn default() -> Self {
PartialServerConfig {
listen_ips: Some(vec!["127.0.0.1".parse().unwrap(), "::1".parse().unwrap()]),
port: Some(3000),
}
}
}
impl FromStr for PartialServerConfig {
type Err = Error;
/// #Examples
/// ```
/// use dpts_config::{
/// PartialServerConfig,
/// PartialDatabaseConfig,
/// };
/// use std::{
/// default::Default,
/// net::IpAddr
/// };
/// let config_from_str: PartialServerConfig = r#"
/// listen_ips = ["0.0.0.0"]
/// port = 8000
/// time_zone = "Asia/Tokyo"
///
/// [database]
/// url = "sqlite::memory:"
/// sqlx_logging = true
/// "#.parse().unwrap();
///
/// let config: PartialServerConfig = PartialServerConfig{
/// listen_ips : Some(vec!["0.0.0.0".parse().unwrap()]),
/// port: Some(8000),
/// time_zone: Some(chrono_tz::Asia::Tokyo),
/// database: Some(PartialDatabaseConfig {
/// url: Some("sqlite::memory:".to_string()),
/// sqlx_logging: Some(true),
/// ..Default::default()
/// }),
/// };
///
/// assert_eq!(config_from_str, config);
/// ```
///
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(toml::from_str(s)?)
}
}

View file

@ -1,10 +1,8 @@
use std::time::Duration;
#[cfg(feature="clap")]
use clap::Args;
use sea_orm::ConnectOptions;
use serde::Deserialize;
use tokio::sync::OnceCell;
use crate::error::Error;
@ -64,26 +62,28 @@ impl TryFrom<PartialDatabaseConfig> for DatabaseConfig{
}
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
#[cfg_attr(feature="clap", derive(Args))]
#[derive(Args)]
pub struct PartialDatabaseConfig {
#[arg(long)]
pub url: Option<String>,
#[arg(long)]
pub max_connections: Option<u32>,
#[arg(long)]
pub min_connections: Option<u32>,
#[cfg_attr(feature="clap", arg(value_parser = parse_duration ))]
#[arg(long, value_parser = parse_duration )]
pub connect_timeout: Option<Duration>,
#[cfg_attr(feature="clap", arg(value_parser = parse_duration ))]
#[arg(long, value_parser = parse_duration )]
pub acquire_timeout: Option<Duration>,
#[cfg_attr(feature="clap", arg(value_parser = parse_duration ))]
#[arg(long, value_parser = parse_duration )]
pub idle_timeout: Option<Duration>,
#[cfg_attr(feature="clap", arg(value_parser = parse_duration ))]
#[arg(long, value_parser = parse_duration )]
pub max_lifetime: Option<Duration>,
#[arg(long)]
pub sqlx_logging: Option<bool>
}
#[cfg(feature="clap")]
fn parse_duration(arg: &str) -> Result<std::time::Duration, Error> {
let seconds = arg.parse()?;
Ok(std::time::Duration::from_secs(seconds))
}
pub static DATABASE_CONFIG: OnceCell<DatabaseConfig> = OnceCell::const_new();

View file

@ -0,0 +1,10 @@
mod database;
pub use database::{
DatabaseConfig,
PartialDatabaseConfig
};
pub struct ServerConfig {}
pub struct PartialServerConfig {}

View file

@ -45,7 +45,6 @@ mod tests {
use std::time::Duration;
use chrono::{offset, FixedOffset, Local, TimeZone};
use sea_orm::{entity::*, query::*, ConnectOptions, Database};
use progress_pile_migration::{ServerMigrator, MigratorTrait};
use crate::{entity::*, global::GLOBAL};
#[tokio::test]

View file

@ -1,4 +1,4 @@
use progress_pile_migration::{ServerMigrator, MigratorTrait};
use progress_pile_migration_server::{Migrator, MigratorTrait};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use crate::error::Error;
use tokio::sync::OnceCell;
@ -19,7 +19,7 @@ impl GlobalDatabase for Global {
{
Ok(self.database.get_or_try_init(|| async {
let db = Database::connect(options).await?;
ServerMigrator::up(&db, None).await?;
Migrator::up(&db, None).await?;
Ok::<DatabaseConnection, Error>(db)
}).await?)
}

View file

@ -1,12 +1,12 @@
mod args;
mod auth;
mod config;
pub mod cli;
pub mod auth;
pub mod config;
pub mod entity;
pub mod global;
pub mod graphql;
pub use progress_pile_core::error;
pub use args::Args;
pub use cli::Cli;
use async_graphql::{EmptySubscription, Schema};
use async_graphql_axum::{
GraphQL,

View file

@ -1,7 +1,7 @@
use async_graphql::{http::{playground_source, GraphQLPlaygroundConfig}, *};
use async_graphql_axum::GraphQL;
use progress_pile_server::{build_app, Args};
use progress_pile_server::{build_app, cli::Cli};
use axum::{response::{Html, IntoResponse}, routing::get, Router};
use clap::Parser;