Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions .github/workflows/migration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Database Migration

on:
push:
branches:
- main
paths:
- Cargo.lock
- Cargo.toml
- crates/migration/**
- rust-toolchain.toml
- .github/workflows/migration.yml
pull_request:
paths:
- Cargo.lock
- Cargo.toml
- crates/migration/**
- rust-toolchain.toml
- .github/workflows/migration.yml

env:
CARGO_TERM_COLOR: always

jobs:
migration:
name: database migration
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: xlair
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U postgres -d xlair"
--health-interval=5s
--health-timeout=5s
--health-retries=5

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo directories
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-migration-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-migration-

- name: Fetch dependencies
run: cargo fetch --locked

- name: Run migrations
run: cargo run -p migration --locked -- up
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/xlair
21 changes: 21 additions & 0 deletions crates/domain/src/entity/asset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use chrono::{DateTime, Utc};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Asset {
key: String,
updated_at: DateTime<Utc>,
}

impl Asset {
pub fn new(key: String, updated_at: DateTime<Utc>) -> Self {
Self { key, updated_at }
}

pub fn key(&self) -> &str {
&self.key
}

pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
}
1 change: 1 addition & 0 deletions crates/domain/src/entity/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod asset;
pub mod clear_type;
pub mod difficulty;
pub mod genre;
Expand Down
14 changes: 7 additions & 7 deletions crates/domain/src/entity/music.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chrono::{DateTime, Utc};
use getset::{Getters, Setters};

use super::genre::Genre;
use super::{asset::Asset, genre::Genre};

#[derive(Debug, Getters, Setters)]
pub struct Music {
Expand All @@ -16,9 +16,9 @@ pub struct Music {
#[getset(get = "pub")]
genre: Genre,
#[getset(get = "pub")]
jacket_key: Option<String>,
jacket: Option<Asset>,
#[getset(get = "pub")]
music_key: Option<String>,
audio: Option<Asset>,
#[getset(get = "pub")]
registration_date: DateTime<Utc>,
#[getset(get = "pub")]
Expand All @@ -33,8 +33,8 @@ impl Music {
artist: String,
bpm: f32,
genre: Genre,
jacket_key: Option<String>,
music_key: Option<String>,
jacket: Option<Asset>,
audio: Option<Asset>,
registration_date: DateTime<Utc>,
is_test: bool,
) -> Self {
Expand All @@ -44,8 +44,8 @@ impl Music {
artist,
bpm,
genre,
jacket_key,
music_key,
jacket,
audio,
registration_date,
is_test,
}
Expand Down
8 changes: 4 additions & 4 deletions crates/domain/src/entity/sheet.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use getset::{Getters, Setters};

use super::{difficulty::Difficulty, level::Level};
use super::{asset::Asset, difficulty::Difficulty, level::Level};

#[derive(Debug, Getters, Setters)]
pub struct Sheet {
Expand All @@ -15,7 +15,7 @@ pub struct Sheet {
#[getset(get = "pub")]
notes_designer: String,
#[getset(get = "pub")]
chart_key: Option<String>,
chart: Option<Asset>,
}

impl Sheet {
Expand All @@ -25,15 +25,15 @@ impl Sheet {
difficulty: Difficulty,
level: Level,
notes_designer: String,
chart_key: Option<String>,
chart: Option<Asset>,
) -> Self {
Self {
id,
music_id,
difficulty,
level,
notes_designer,
chart_key,
chart,
}
}
}
4 changes: 2 additions & 2 deletions crates/domain/src/repository/music.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,10 @@ pub trait MusicRepository: Send + Sync {
jacket_key: Option<String>,
) -> impl Future<Output = Result<MusicWithSheets, MusicRepositoryError>> + Send;

fn update_music_key(
fn update_audio_key(
&self,
music_id: &str,
music_key: Option<String>,
audio_key: Option<String>,
) -> impl Future<Output = Result<MusicWithSheets, MusicRepositoryError>> + Send;

fn update_chart_key(
Expand Down
4 changes: 3 additions & 1 deletion crates/infrastructure/src/entities/musics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ pub struct Model {
pub bpm: Decimal,
pub genre: i32,
pub jacket_key: Option<String>,
pub music_key: Option<String>,
pub audio_key: Option<String>,
pub jacket_updated_at: Option<DateTimeWithTimeZone>,
pub audio_updated_at: Option<DateTimeWithTimeZone>,
pub registration_date: DateTimeWithTimeZone,
pub is_test: bool,
}
Expand Down
1 change: 1 addition & 0 deletions crates/infrastructure/src/entities/sheets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Model {
pub level: i32,
pub notes_designer: String,
pub chart_key: Option<String>,
pub chart_updated_at: Option<DateTimeWithTimeZone>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
Expand Down
6 changes: 3 additions & 3 deletions crates/infrastructure/src/music/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ impl MusicRepository for MusicRepositoryImpl {
write::update_jacket_key(self.db.as_ref(), music_id, jacket_key).await
}

async fn update_music_key(
async fn update_audio_key(
&self,
music_id: &str,
music_key: Option<String>,
audio_key: Option<String>,
) -> Result<MusicWithSheets, MusicRepositoryError> {
write::update_music_key(self.db.as_ref(), music_id, music_key).await
write::update_audio_key(self.db.as_ref(), music_id, audio_key).await
}

async fn update_chart_key(
Expand Down
63 changes: 59 additions & 4 deletions crates/infrastructure/src/music/read_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::convert::TryFrom;
use anyhow::{Error as AnyError, anyhow};
use chrono::Utc;
use domain::{
entity::{difficulty::Difficulty, genre::Genre, level::Level, music::Music, sheet::Sheet},
entity::{
asset::Asset, difficulty::Difficulty, genre::Genre, level::Level, music::Music,
sheet::Sheet,
},
repository::music::MusicRepositoryError,
};
use sea_orm::prelude::Decimal;
Expand All @@ -19,14 +22,16 @@ pub fn convert_music(model: MusicModel) -> Result<Music, MusicRepositoryError> {
let genre = convert_genre(model.genre)?;
let registration_date = model.registration_date.with_timezone(&Utc);

let jacket = asset(model.jacket_key, model.jacket_updated_at)?;
let audio = asset(model.audio_key, model.audio_updated_at)?;
Ok(Music::new(
model.id.to_string(),
model.title,
model.artist,
bpm,
genre,
model.jacket_key,
model.music_key,
jacket,
audio,
registration_date,
model.is_test,
))
Expand All @@ -44,16 +49,30 @@ fn convert_sheet(model: SheetModel) -> Result<Sheet, MusicRepositoryError> {
let difficulty = convert_difficulty(model.difficulty);
let level = convert_level(model.level)?;

let chart = asset(model.chart_key, model.chart_updated_at)?;
Ok(Sheet::new(
model.id.to_string(),
model.music_id.to_string(),
difficulty,
level,
model.notes_designer,
model.chart_key,
chart,
))
}

fn asset(
key: Option<String>,
updated_at: Option<sea_orm::prelude::DateTimeWithTimeZone>,
) -> Result<Option<Asset>, MusicRepositoryError> {
match (key, updated_at) {
(Some(key), Some(updated_at)) => Ok(Some(Asset::new(key, updated_at.with_timezone(&Utc)))),
(None, None) => Ok(None),
_ => Err(MusicRepositoryError::InternalError(AnyError::msg(
"asset key and updated_at must be present together",
))),
}
}

fn convert_bpm(bpm: Decimal) -> Result<f32, MusicRepositoryError> {
let bpm_str = bpm.to_string();
bpm_str.parse::<f32>().map_err(|err| {
Expand Down Expand Up @@ -107,3 +126,39 @@ fn convert_difficulty(value: DbDifficulty) -> Difficulty {
DbDifficulty::Master => Difficulty::Master,
}
}

#[cfg(test)]
mod tests {
use chrono::{FixedOffset, TimeZone, Utc};

use super::*;

#[test]
fn asset_requires_key_and_updated_at_together() {
let updated_at = Utc
.with_ymd_and_hms(2025, 10, 1, 12, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap());

assert!(asset(Some("jacket.png".to_owned()), None).is_err());
assert!(asset(None, Some(updated_at)).is_err());
}

#[test]
fn asset_converts_database_values() {
let updated_at = Utc
.with_ymd_and_hms(2025, 10, 1, 12, 0, 0)
.unwrap()
.with_timezone(&FixedOffset::east_opt(9 * 60 * 60).unwrap());

let converted = asset(Some("jacket.png".to_owned()), Some(updated_at))
.unwrap()
.unwrap();

assert_eq!(converted.key(), "jacket.png");
assert_eq!(
converted.updated_at(),
Utc.with_ymd_and_hms(2025, 10, 1, 12, 0, 0).unwrap()
);
}
}
10 changes: 5 additions & 5 deletions crates/infrastructure/src/music/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, DbConn, EntityTrait, QueryFilter, T
use tracing::error;

use super::write_adapter::{
music_active_model_for_insert, music_active_model_for_jacket_key,
music_active_model_for_music_key, music_active_model_for_update,
music_active_model_for_audio_key, music_active_model_for_insert,
music_active_model_for_jacket_key, music_active_model_for_update,
sheet_active_model_for_chart_key, sheet_active_model_for_insert, sheet_active_model_for_update,
};
use crate::entities;
Expand Down Expand Up @@ -96,12 +96,12 @@ pub async fn update_jacket_key(
super::read::find_with_sheets(db, music_id).await
}

pub async fn update_music_key(
pub async fn update_audio_key(
db: &DbConn,
music_id: &str,
music_key: Option<String>,
audio_key: Option<String>,
) -> Result<MusicWithSheets, MusicRepositoryError> {
music_active_model_for_music_key(music_id, music_key)?
music_active_model_for_audio_key(music_id, audio_key)?
.update(db)
.await
.map_err(internal)?;
Expand Down
Loading
Loading