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
7 changes: 7 additions & 0 deletions crates/domain/src/repository/music.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::entity::{music::Music, sheet::Sheet};

#[derive(Debug, Error)]
pub enum MusicRepositoryError {
#[error("Music not found: {0}")]
NotFound(String),
#[error(transparent)]
InternalError(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -47,4 +49,9 @@ pub trait MusicRepository: Send + Sync {
cursor: Option<MusicListCursor>,
limit: u64,
) -> impl Future<Output = Result<MusicListPage, MusicRepositoryError>> + Send;

fn find_with_sheets(
&self,
music_id: &str,
) -> impl Future<Output = Result<MusicWithSheets, MusicRepositoryError>> + Send;
}
9 changes: 9 additions & 0 deletions crates/infrastructure/src/music/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ impl MusicRepository for MusicRepositoryImpl {
debug!(limit, "Loading a page of music metadata via SeaORM");
read::list_with_sheets_page(self.db.as_ref(), cursor, limit).await
}

#[instrument(skip(self), fields(music_id = %music_id))]
async fn find_with_sheets(
&self,
music_id: &str,
) -> Result<MusicWithSheets, MusicRepositoryError> {
debug!("Loading music metadata by id via SeaORM");
read::find_with_sheets(self.db.as_ref(), music_id).await
}
}
28 changes: 28 additions & 0 deletions crates/infrastructure/src/music/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,31 @@ pub async fn list_with_sheets_page(

Ok(MusicListPage { items, next_cursor })
}

/// Loads one music entry with all related sheets.
pub async fn find_with_sheets(
db: &DbConn,
music_id: &str,
) -> Result<MusicWithSheets, MusicRepositoryError> {
let id = uuid::Uuid::parse_str(music_id)
.map_err(|error| MusicRepositoryError::InternalError(AnyError::from(error)))?;
let result = entities::musics::Entity::find_by_id(id)
.find_with_related(entities::sheets::Entity)
.all(db)
.await
.map_err(|err| {
error!(error = %err, music_id, "Failed to fetch music by id");
MusicRepositoryError::InternalError(AnyError::from(err))
})?
.into_iter()
.next();

let Some((music_model, sheet_models)) = result else {
return Err(MusicRepositoryError::NotFound(music_id.to_owned()));
};

Ok(MusicWithSheets::new(
adapter::convert_music(music_model)?,
adapter::convert_sheets(sheet_models)?,
))
}
4 changes: 4 additions & 0 deletions crates/presentation/src/error/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ impl From<UserUsecaseError> for AppError {
impl From<MusicRepositoryError> for AppError {
fn from(error: MusicRepositoryError) -> Self {
match error {
MusicRepositoryError::NotFound(id) => AppError {
status_code: axum::http::StatusCode::NOT_FOUND,
message: format!("Music not found: {id}"),
},
MusicRepositoryError::InternalError(err) => AppError {
status_code: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
message: err.to_string(),
Expand Down
10 changes: 9 additions & 1 deletion crates/presentation/src/route/admin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use axum::{
Json,
extract::{Query, State},
extract::{Path, Query, State},
};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Utc};
Expand Down Expand Up @@ -58,6 +58,14 @@ pub async fn handle_list_musics(
Ok(Json(MusicListResponse { items, next_cursor }))
}

pub async fn handle_get_music(
State(state): State<crate::state::State>,
Path(music_id): Path<String>,
) -> Result<Json<SyncItemResponse>, AppError> {
let music = state.usecases.music.find_by_id(music_id).await?;
Ok(Json(SyncItemResponse::from(music)))
}

fn encode_cursor(cursor: MusicListCursor) -> Result<String, AppError> {
let payload = CursorPayload {
registration_date: cursor.registration_date.to_rfc3339(),
Expand Down
1 change: 1 addition & 0 deletions crates/presentation/src/route/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub fn create_app(state: State, authenticator: Option<Authenticator>) -> Router
let health = Router::new().route("/", get(|| async { "OK" }));
let admin_routes = Router::new()
.route("/musics", get(admin::handle_list_musics))
.route("/musics/{musicId}", get(admin::handle_get_music))
.route("/db/synchronize", post(admin::handle_db_synchronization));

let private_routes = Router::new()
Expand Down
47 changes: 47 additions & 0 deletions crates/usecase/src/music/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ impl<R: Repositories> MusicUsecase<R> {
next_cursor: page.next_cursor,
})
}

pub async fn find_by_id(
&self,
music_id: String,
) -> Result<MusicWithSheetsDto, MusicUsecaseError> {
let music = self
.repositories
.music()
.find_with_sheets(&music_id)
.await?;
Ok(music.into())
}
}

pub struct MusicPageDto {
Expand Down Expand Up @@ -121,4 +133,39 @@ mod tests {
assert_eq!(result[0].sheets.len(), 1);
assert_eq!(result[0].sheets[0].id, "sheet-1");
}

#[tokio::test]
async fn find_by_id_returns_entry() {
let mut music_repo = MockMusicRepository::new();
music_repo
.expect_find_with_sheets()
.withf(|music_id| music_id == "music-1")
.returning(|_| {
let music = Music::new(
"music-1".to_owned(),
"Song".to_owned(),
"Artist".to_owned(),
135.5,
Genre::ORIGINAL,
"jacket.png".to_owned(),
Utc::now(),
false,
);
Box::pin(async move { Ok(MusicWithSheets::new(music, Vec::new())) })
});

let repositories = MockRepositories {
user: MockUserRepository::new(),
record: MockRecordRepository::new(),
music: music_repo,
};
let usecase = MusicUsecase::new(Arc::new(repositories));

let result = usecase
.find_by_id("music-1".to_owned())
.await
.expect("should succeed");
assert_eq!(result.music.id, "music-1");
assert!(result.sheets.is_empty());
}
}
30 changes: 30 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,36 @@ paths:
description: Forbidden - The authenticated user is not an administrator
"500":
description: Internal server error
/admin/musics/{musicId}:
get:
tags:
- admin
summary: 楽曲と譜面の詳細を取得
security:
- userAuth: []
parameters:
- name: musicId
in: path
description: 楽曲のID
required: true
schema:
type: string
format: uuid
responses:
"200":
description: 楽曲と譜面の詳細
content:
application/json:
schema:
$ref: "#/components/schemas/musicWithSheets"
"401":
description: Unauthorized - Invalid access token
"403":
description: Forbidden - The authenticated user is not an administrator
"404":
description: Not found - Music not found
"500":
description: Internal server error
/users/{userId}/credits/increment:
post:
tags:
Expand Down
Loading