diff --git a/musicus_server/src/database/ensembles.rs b/musicus_server/src/database/ensembles.rs index 8bb8468..9330259 100644 --- a/musicus_server/src/database/ensembles.rs +++ b/musicus_server/src/database/ensembles.rs @@ -1,75 +1,99 @@ use super::schema::ensembles; -use super::DbConn; -use anyhow::Result; +use super::{DbConn, User}; +use crate::error::ServerError; +use anyhow::{Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -/// An ensemble that takes part in recordings. -#[derive(Insertable, Queryable, Serialize, Debug, Clone)] +/// A ensemble as represented within the API. +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Ensemble { + pub id: u32, + pub name: String, +} + +/// A ensemble as represented in the database. +#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] +#[table_name = "ensembles"] +struct EnsembleRow { pub id: i64, pub name: String, - - #[serde(skip)] pub created_by: String, } -/// A structure representing data on an ensemble. -#[derive(AsChangeset, Deserialize, Debug, Clone)] -#[table_name = "ensembles"] -#[serde(rename_all = "camelCase")] -pub struct EnsembleInsertion { - pub name: String, +impl From for Ensemble { + fn from(row: EnsembleRow) -> Ensemble { + Ensemble { + id: row.id as u32, + name: row.name, + } + } } -/// Insert a new ensemble. -pub fn insert_ensemble( - conn: &DbConn, - id: u32, - data: &EnsembleInsertion, - created_by: &str, -) -> Result<()> { - let ensemble = Ensemble { - id: id as i64, - name: data.name.clone(), - created_by: created_by.to_string(), +/// Update an existing ensemble or insert a new one. This will only work, if the provided user is +/// allowed to do that. +pub fn update_ensemble(conn: &DbConn, ensemble: &Ensemble, user: &User) -> Result<()> { + let old_row = get_ensemble_row(conn, ensemble.id)?; + + let allowed = match old_row { + Some(row) => user.may_edit(&row.created_by), + None => user.may_create(), }; - diesel::insert_into(ensembles::table) - .values(ensemble) - .execute(conn)?; + if allowed { + let new_row = EnsembleRow { + id: ensemble.id as i64, + name: ensemble.name.clone(), + created_by: user.username.clone(), + }; - Ok(()) -} + diesel::insert_into(ensembles::table) + .values(&new_row) + .on_conflict(ensembles::id) + .do_update() + .set(&new_row) + .execute(conn)?; -/// Update an existing ensemble. -pub fn update_ensemble(conn: &DbConn, id: u32, data: &EnsembleInsertion) -> Result<()> { - diesel::update(ensembles::table) - .filter(ensembles::id.eq(id as i64)) - .set(data) - .execute(conn)?; - - Ok(()) + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get an existing ensemble. pub fn get_ensemble(conn: &DbConn, id: u32) -> Result> { - Ok(ensembles::table - .filter(ensembles::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned()) + let row = get_ensemble_row(conn, id)?; + let ensemble = row.map(|row| row.into()); + + Ok(ensemble) } -/// Delete an existing ensemble. -pub fn delete_ensemble(conn: &DbConn, id: u32) -> Result<()> { - diesel::delete(ensembles::table.filter(ensembles::id.eq(id as i64))).execute(conn)?; - Ok(()) +/// Delete an existing ensemble. This will only work if the provided user is allowed to do that. +pub fn delete_ensemble(conn: &DbConn, id: u32, user: &User) -> Result<()> { + if user.may_delete() { + diesel::delete(ensembles::table.filter(ensembles::id.eq(id as i64))).execute(conn)?; + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get all existing ensembles. pub fn get_ensembles(conn: &DbConn) -> Result> { - Ok(ensembles::table.load::(conn)?) + let rows = ensembles::table.load::(conn)?; + let ensembles: Vec = rows.into_iter().map(|row| row.into()).collect(); + + Ok(ensembles) +} + +/// Get a ensemble row if it exists. +fn get_ensemble_row(conn: &DbConn, id: u32) -> Result> { + let row = ensembles::table + .filter(ensembles::id.eq(id as i64)) + .load::(conn)? + .into_iter() + .next(); + + Ok(row) } diff --git a/musicus_server/src/database/instruments.rs b/musicus_server/src/database/instruments.rs index 8e32f77..a279a4d 100644 --- a/musicus_server/src/database/instruments.rs +++ b/musicus_server/src/database/instruments.rs @@ -1,75 +1,99 @@ use super::schema::instruments; -use super::DbConn; -use anyhow::Result; +use super::{DbConn, User}; +use crate::error::ServerError; +use anyhow::{Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -/// An instrument or any other possible role within a recording. -#[derive(Insertable, Queryable, Serialize, Debug, Clone)] +/// A instrument as represented within the API. +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Instrument { + pub id: u32, + pub name: String, +} + +/// A instrument as represented in the database. +#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] +#[table_name = "instruments"] +struct InstrumentRow { pub id: i64, pub name: String, - - #[serde(skip)] pub created_by: String, } -/// A structure representing data on an instrument. -#[derive(AsChangeset, Deserialize, Debug, Clone)] -#[table_name = "instruments"] -#[serde(rename_all = "camelCase")] -pub struct InstrumentInsertion { - pub name: String, +impl From for Instrument { + fn from(row: InstrumentRow) -> Instrument { + Instrument { + id: row.id as u32, + name: row.name, + } + } } -/// Insert a new instrument. -pub fn insert_instrument( - conn: &DbConn, - id: u32, - data: &InstrumentInsertion, - created_by: &str, -) -> Result<()> { - let instrument = Instrument { - id: id as i64, - name: data.name.clone(), - created_by: created_by.to_string(), +/// Update an existing instrument or insert a new one. This will only work, if the provided user is +/// allowed to do that. +pub fn update_instrument(conn: &DbConn, instrument: &Instrument, user: &User) -> Result<()> { + let old_row = get_instrument_row(conn, instrument.id)?; + + let allowed = match old_row { + Some(row) => user.may_edit(&row.created_by), + None => user.may_create(), }; - diesel::insert_into(instruments::table) - .values(instrument) - .execute(conn)?; + if allowed { + let new_row = InstrumentRow { + id: instrument.id as i64, + name: instrument.name.clone(), + created_by: user.username.clone(), + }; - Ok(()) -} + diesel::insert_into(instruments::table) + .values(&new_row) + .on_conflict(instruments::id) + .do_update() + .set(&new_row) + .execute(conn)?; -/// Update an existing instrument. -pub fn update_instrument(conn: &DbConn, id: u32, data: &InstrumentInsertion) -> Result<()> { - diesel::update(instruments::table) - .filter(instruments::id.eq(id as i64)) - .set(data) - .execute(conn)?; - - Ok(()) + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get an existing instrument. pub fn get_instrument(conn: &DbConn, id: u32) -> Result> { - Ok(instruments::table - .filter(instruments::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned()) + let row = get_instrument_row(conn, id)?; + let instrument = row.map(|row| row.into()); + + Ok(instrument) } -/// Delete an existing instrument. -pub fn delete_instrument(conn: &DbConn, id: u32) -> Result<()> { - diesel::delete(instruments::table.filter(instruments::id.eq(id as i64))).execute(conn)?; - Ok(()) +/// Delete an existing instrument. This will only work if the provided user is allowed to do that. +pub fn delete_instrument(conn: &DbConn, id: u32, user: &User) -> Result<()> { + if user.may_delete() { + diesel::delete(instruments::table.filter(instruments::id.eq(id as i64))).execute(conn)?; + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get all existing instruments. pub fn get_instruments(conn: &DbConn) -> Result> { - Ok(instruments::table.load::(conn)?) -} \ No newline at end of file + let rows = instruments::table.load::(conn)?; + let instruments: Vec = rows.into_iter().map(|row| row.into()).collect(); + + Ok(instruments) +} + +/// Get a instrument row if it exists. +fn get_instrument_row(conn: &DbConn, id: u32) -> Result> { + let row = instruments::table + .filter(instruments::id.eq(id as i64)) + .load::(conn)? + .into_iter() + .next(); + + Ok(row) +} diff --git a/musicus_server/src/database/persons.rs b/musicus_server/src/database/persons.rs index 1e54155..001f59c 100644 --- a/musicus_server/src/database/persons.rs +++ b/musicus_server/src/database/persons.rs @@ -1,78 +1,103 @@ use super::schema::persons; -use super::DbConn; -use anyhow::Result; +use super::{DbConn, User}; +use crate::error::ServerError; +use anyhow::{Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -/// A person that is a composer, an interpret or both. -#[derive(Insertable, Queryable, Serialize, Debug, Clone)] +/// A person as represented within the API. +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Person { + pub id: u32, + pub first_name: String, + pub last_name: String, +} + +/// A person as represented in the database. +#[derive(Insertable, Queryable, AsChangeset, Debug, Clone)] +#[table_name = "persons"] +struct PersonRow { pub id: i64, pub first_name: String, pub last_name: String, - - #[serde(skip)] pub created_by: String, } -/// A structure representing data on a person. -#[derive(AsChangeset, Deserialize, Debug, Clone)] -#[table_name = "persons"] -#[serde(rename_all = "camelCase")] -pub struct PersonInsertion { - pub first_name: String, - pub last_name: String, +impl From for Person { + fn from(row: PersonRow) -> Person { + Person { + id: row.id as u32, + first_name: row.first_name, + last_name: row.last_name, + } + } } -/// Insert a new person. -pub fn insert_person( - conn: &DbConn, - id: u32, - data: &PersonInsertion, - created_by: &str, -) -> Result<()> { - let person = Person { - id: id as i64, - first_name: data.first_name.clone(), - last_name: data.last_name.clone(), - created_by: created_by.to_string(), +/// Update an existing person or insert a new one. This will only work, if the provided user is +/// allowed to do that. +pub fn update_person(conn: &DbConn, person: &Person, user: &User) -> Result<()> { + let old_row = get_person_row(conn, person.id)?; + + let allowed = match old_row { + Some(row) => user.may_edit(&row.created_by), + None => user.may_create(), }; - diesel::insert_into(persons::table) - .values(person) - .execute(conn)?; + if allowed { + let new_row = PersonRow { + id: person.id as i64, + first_name: person.first_name.clone(), + last_name: person.last_name.clone(), + created_by: user.username.clone(), + }; - Ok(()) -} + diesel::insert_into(persons::table) + .values(&new_row) + .on_conflict(persons::id) + .do_update() + .set(&new_row) + .execute(conn)?; -/// Update an existing person. -pub fn update_person(conn: &DbConn, id: u32, data: &PersonInsertion) -> Result<()> { - diesel::update(persons::table) - .filter(persons::id.eq(id as i64)) - .set(data) - .execute(conn)?; - - Ok(()) + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get an existing person. pub fn get_person(conn: &DbConn, id: u32) -> Result> { - Ok(persons::table - .filter(persons::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned()) + let row = get_person_row(conn, id)?; + let person = row.map(|row| row.into()); + + Ok(person) } -/// Delete an existing person. -pub fn delete_person(conn: &DbConn, id: u32) -> Result<()> { - diesel::delete(persons::table.filter(persons::id.eq(id as i64))).execute(conn)?; - Ok(()) +/// Delete an existing person. This will only work if the provided user is allowed to do that. +pub fn delete_person(conn: &DbConn, id: u32, user: &User) -> Result<()> { + if user.may_delete() { + diesel::delete(persons::table.filter(persons::id.eq(id as i64))).execute(conn)?; + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } } /// Get all existing persons. pub fn get_persons(conn: &DbConn) -> Result> { - Ok(persons::table.load::(conn)?) + let rows = persons::table.load::(conn)?; + let persons: Vec = rows.into_iter().map(|row| row.into()).collect(); + + Ok(persons) +} + +/// Get a person row if it exists. +fn get_person_row(conn: &DbConn, id: u32) -> Result> { + let row = persons::table + .filter(persons::id.eq(id as i64)) + .load::(conn)? + .into_iter() + .next(); + + Ok(row) } diff --git a/musicus_server/src/database/recordings.rs b/musicus_server/src/database/recordings.rs index 8f88c62..dde7233 100644 --- a/musicus_server/src/database/recordings.rs +++ b/musicus_server/src/database/recordings.rs @@ -1,23 +1,44 @@ -use super::schema::{ensembles, instruments, performances, persons, recordings}; -use super::{get_work_description, DbConn, Ensemble, Instrument, Person, WorkDescription}; +use super::schema::{ensembles, performances, persons, recordings}; +use super::{get_ensemble, get_instrument, get_person, get_work}; +use super::{DbConn, Ensemble, Instrument, Person, User, Work}; +use crate::error::ServerError; use anyhow::{anyhow, Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; /// A specific recording of a work. -#[derive(Insertable, Queryable, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct Recording { + pub id: u32, + pub work: Work, + pub comment: String, + pub performances: Vec, +} + +/// How a person or ensemble was involved in a recording. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Performance { + pub person: Option, + pub ensemble: Option, + pub role: Option, +} + +/// Row data for a recording. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "recordings"] +struct RecordingRow { pub id: i64, pub work: i64, pub comment: String, pub created_by: String, } -/// How a person or ensemble was involved in a recording. +/// Row data for a performance. #[derive(Insertable, Queryable, Debug, Clone)] -pub struct Performance { +#[table_name = "performances"] +struct PerformanceRow { pub id: i64, pub recording: i64, pub person: Option, @@ -25,254 +46,189 @@ pub struct Performance { pub role: Option, } -/// A structure for collecting all available information on a performance. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct PerformanceDescription { - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// A structure for collecting all available information on a recording. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RecordingDescription { - pub id: i64, - pub work: WorkDescription, - pub comment: String, - pub performances: Vec, -} - -/// A structure representing data on a performance. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct PerformanceInsertion { - pub person: Option, - pub ensemble: Option, - pub role: Option, -} - -/// A bundle of everything needed for adding a new recording to the database. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RecordingInsertion { - pub work: i64, - pub comment: String, - pub performances: Vec, -} - -/// Insert a new recording. -pub fn insert_recording( - conn: &DbConn, - id: u32, - data: &RecordingInsertion, - created_by: &str, -) -> Result<()> { +/// Update an existing recording or insert a new one. This will only work, if the provided user is +/// allowed to do that. +// TODO: Also add newly created associated items. +pub fn update_recording(conn: &DbConn, recording: &Recording, user: &User) -> Result<()> { conn.transaction::<(), Error, _>(|| { - let id = id as i64; + let old_row = get_recording_row(conn, recording.id)?; - diesel::insert_into(recordings::table) - .values(Recording { + let allowed = match old_row { + Some(row) => user.may_edit(&row.created_by), + None => user.may_create(), + }; + + if allowed { + let id = recording.id as i64; + + // This will also delete the old performances. + diesel::delete(recordings::table) + .filter(recordings::id.eq(id)) + .execute(conn)?; + + let row = RecordingRow { id, - work: data.work, - comment: data.comment.clone(), - created_by: created_by.to_string(), - }) - .execute(conn)?; + work: recording.work.id as i64, + comment: recording.comment.clone(), + created_by: user.username.clone(), + }; - insert_recording_data(conn, id, data)?; + diesel::insert_into(recordings::table) + .values(row) + .execute(conn)?; - Ok(()) + for performance in &recording.performances { + diesel::insert_into(performances::table) + .values(PerformanceRow { + id: rand::random(), + recording: id, + person: performance.person.as_ref().map(|person| person.id as i64), + ensemble: performance + .ensemble + .as_ref() + .map(|ensemble| ensemble.id as i64), + role: performance.role.as_ref().map(|role| role.id as i64), + }) + .execute(conn)?; + } + + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } })?; Ok(()) } -/// Update an existing recording. -pub fn update_recording(conn: &DbConn, id: u32, data: &RecordingInsertion) -> Result<()> { - conn.transaction::<(), Error, _>(|| { - let id = id as i64; +/// Get an existing recording and all available information from related tables. +pub fn get_recording(conn: &DbConn, id: u32) -> Result> { + let recording = match get_recording_row(conn, id)? { + Some(row) => Some(get_description_for_recording_row(conn, &row)?), + None => None, + }; - diesel::delete(performances::table) - .filter(performances::recording.eq(id)) - .execute(conn)?; - - diesel::update(recordings::table) - .filter(recordings::id.eq(id)) - .set(( - recordings::work.eq(data.work), - recordings::comment.eq(data.comment.clone()), - )) - .execute(conn)?; - - insert_recording_data(conn, id, data)?; - - Ok(()) - })?; - - Ok(()) + Ok(recording) } -/// Helper method to populate other tables related to a recording. -fn insert_recording_data(conn: &DbConn, id: i64, data: &RecordingInsertion) -> Result<()> { - for performance in &data.performances { - diesel::insert_into(performances::table) - .values(Performance { - id: rand::random(), - recording: id, - person: performance.person, - ensemble: performance.ensemble, - role: performance.role, - }) - .execute(conn)?; +/// Get all available information on all recordings where a person is performing. +pub fn get_recordings_for_person(conn: &DbConn, person_id: u32) -> Result> { + let mut recordings: Vec = Vec::new(); + + let rows = recordings::table + .inner_join(performances::table.on(performances::recording.eq(recordings::id))) + .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) + .filter(persons::id.eq(person_id as i64)) + .select(recordings::table::all_columns()) + .load::(conn)?; + + for row in rows { + recordings.push(get_description_for_recording_row(conn, &row)?); } - Ok(()) + Ok(recordings) } -/// Get an existing recording. -pub fn get_recording(conn: &DbConn, id: u32) -> Result> { +/// Get all available information on all recordings where an ensemble is performing. +pub fn get_recordings_for_ensemble(conn: &DbConn, ensemble_id: u32) -> Result> { + let mut recordings: Vec = Vec::new(); + + let rows = recordings::table + .inner_join(performances::table.on(performances::recording.eq(recordings::id))) + .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) + .filter(ensembles::id.eq(ensemble_id as i64)) + .select(recordings::table::all_columns()) + .load::(conn)?; + + for row in rows { + recordings.push(get_description_for_recording_row(conn, &row)?); + } + + Ok(recordings) +} + +/// Get allavailable information on all recordings of a work. +pub fn get_recordings_for_work(conn: &DbConn, work_id: u32) -> Result> { + let mut recordings: Vec = Vec::new(); + + let rows = recordings::table + .filter(recordings::work.eq(work_id as i64)) + .load::(conn)?; + + for row in rows { + recordings.push(get_description_for_recording_row(conn, &row)?); + } + + Ok(recordings) +} + +/// Delete an existing recording. This will fail if there are still references to this +/// recording from other tables that are not directly part of the recording data. Also, the +/// provided user has to be allowed to delete the recording. +pub fn delete_recording(conn: &DbConn, id: u32, user: &User) -> Result<()> { + if user.may_delete() { + diesel::delete(recordings::table.filter(recordings::id.eq(id as i64))).execute(conn)?; + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } +} + +/// Get an existing recording row. +fn get_recording_row(conn: &DbConn, id: u32) -> Result> { Ok(recordings::table .filter(recordings::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned()) + .load::(conn)? + .into_iter() + .next()) } /// Retrieve all available information on a recording from related tables. -pub fn get_description_for_recording( - conn: &DbConn, - recording: &Recording, -) -> Result { - let mut performance_descriptions: Vec = Vec::new(); +fn get_description_for_recording_row(conn: &DbConn, row: &RecordingRow) -> Result { + let mut performances: Vec = Vec::new(); - let performances = performances::table - .filter(performances::recording.eq(recording.id)) - .load::(conn)?; + let performance_rows = performances::table + .filter(performances::recording.eq(row.id)) + .load::(conn)?; - for performance in performances { - performance_descriptions.push(PerformanceDescription { - person: match performance.person { - Some(id) => Some( - persons::table - .filter(persons::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned() - .ok_or(anyhow!("No person with ID: {}", id))?, - ), + for row in performance_rows { + performances.push(Performance { + person: match row.person { + Some(id) => { + let id = id as u32; + Some(get_person(conn, id)?.ok_or(anyhow!("No person with ID: {}", id))?) + } None => None, }, - ensemble: match performance.ensemble { - Some(id) => Some( - ensembles::table - .filter(ensembles::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned() - .ok_or(anyhow!("No ensemble with ID: {}", id))?, - ), + ensemble: match row.ensemble { + Some(id) => { + let id = id as u32; + Some(get_ensemble(conn, id)?.ok_or(anyhow!("No ensemble with ID: {}", id))?) + } None => None, }, - role: match performance.role { - Some(id) => Some( - instruments::table - .filter(instruments::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned() - .ok_or(anyhow!("No instrument with ID: {}", id))?, - ), + role: match row.role { + Some(id) => { + let id = id as u32; + Some( + get_instrument(conn, id)? + .ok_or(anyhow!("No instrument with ID: {}", id))?, + ) + } None => None, }, }); } - let work_id = recording.work.try_into()?; - let work = - get_work_description(conn, work_id)?.ok_or(anyhow!("Work doesn't exist: {}", work_id))?; + let id = row.work as u32; + let work = get_work(conn, id)?.ok_or(anyhow!("No work with ID: {}", id))?; - let recording_description = RecordingDescription { - id: recording.id, + let recording = Recording { + id: row.id as u32, work, - comment: recording.comment.clone(), - performances: performance_descriptions, + comment: row.comment.clone(), + performances, }; - Ok(recording_description) -} - -/// Get an existing recording and all available information from related tables. -pub fn get_recording_description(conn: &DbConn, id: u32) -> Result> { - let recording_description = match get_recording(conn, id)? { - Some(recording) => Some(get_description_for_recording(conn, &recording)?), - None => None, - }; - - Ok(recording_description) -} - -/// Get all available information on all recordings where a person is performing. -pub fn get_recordings_for_person( - conn: &DbConn, - person_id: u32, -) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(persons::table.on(persons::id.nullable().eq(performances::person))) - .filter(persons::id.eq(person_id as i64)) - .select(recordings::table::all_columns()) - .load::(conn)?; - - for recording in recordings { - recording_descriptions.push(get_description_for_recording(conn, &recording)?); - } - - Ok(recording_descriptions) -} - -/// Get all available information on all recordings where an ensemble is performing. -pub fn get_recordings_for_ensemble( - conn: &DbConn, - ensemble_id: u32, -) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = recordings::table - .inner_join(performances::table.on(performances::recording.eq(recordings::id))) - .inner_join(ensembles::table.on(ensembles::id.nullable().eq(performances::ensemble))) - .filter(ensembles::id.eq(ensemble_id as i64)) - .select(recordings::table::all_columns()) - .load::(conn)?; - - for recording in recordings { - recording_descriptions.push(get_description_for_recording(conn, &recording)?); - } - - Ok(recording_descriptions) -} - -/// Get allavailable information on all recordings of a work. -pub fn get_recordings_for_work(conn: &DbConn, work_id: u32) -> Result> { - let mut recording_descriptions: Vec = Vec::new(); - - let recordings = recordings::table - .filter(recordings::work.eq(work_id as i64)) - .load::(conn)?; - - for recording in recordings { - recording_descriptions.push(get_description_for_recording(conn, &recording)?); - } - - Ok(recording_descriptions) -} - -/// Delete an existing recording. This will fail if there are still references to this -/// recording from other tables that are not directly part of the recording data. -pub fn delete_recording(conn: &DbConn, id: u32) -> Result<()> { - diesel::delete(recordings::table.filter(recordings::id.eq(id as i64))).execute(conn)?; - Ok(()) + Ok(recording) } diff --git a/musicus_server/src/database/users.rs b/musicus_server/src/database/users.rs index ce06229..14c874b 100644 --- a/musicus_server/src/database/users.rs +++ b/musicus_server/src/database/users.rs @@ -15,6 +15,23 @@ pub struct User { pub is_banned: bool, } +impl User { + /// Check whether the user is allowed to create a new item. + pub fn may_create(&self) -> bool { + !self.is_banned + } + + /// Check whether the user is allowed to edit an item created by him or somebody else. + pub fn may_edit(&self, creator: &str) -> bool { + !self.is_banned && (self.username == creator || self.is_editor) + } + + /// Check whether the user is allowed to delete an item. + pub fn may_delete(&self) -> bool { + !self.is_banned && self.is_editor + } +} + /// A structure representing data on a user. #[derive(AsChangeset, Deserialize, Debug, Clone)] #[table_name = "users"] diff --git a/musicus_server/src/database/works.rs b/musicus_server/src/database/works.rs index ca5630e..cacc866 100644 --- a/musicus_server/src/database/works.rs +++ b/musicus_server/src/database/works.rs @@ -1,31 +1,62 @@ -use super::schema::{instrumentations, instruments, persons, work_parts, work_sections, works}; -use super::{get_person, DbConn, Instrument, Person}; +use super::schema::{instrumentations, work_parts, work_sections, works}; +use super::{get_instrument, get_person, DbConn, Instrument, Person, User}; +use crate::error::ServerError; use anyhow::{anyhow, Error, Result}; use diesel::prelude::*; -use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -/// A composition by a composer. -#[derive(Insertable, Queryable, Debug, Clone)] +/// A specific work by a composer. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] pub struct Work { + pub id: u32, + pub title: String, + pub composer: Person, + pub instruments: Vec, + pub parts: Vec, + pub sections: Vec, +} + +/// A playable part of a work. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkPart { + pub title: String, + pub composer: Option, +} + +/// A heading within the work structure. +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkSection { + pub title: String, + pub before_index: i64, +} + +/// Table data for a work. +#[derive(Insertable, Queryable, Debug, Clone)] +#[table_name = "works"] +struct WorkRow { pub id: i64, pub composer: i64, pub title: String, pub created_by: String, } -/// Definition that a work uses an instrument. +/// Table data for an instrumentation. #[derive(Insertable, Queryable, Debug, Clone)] -pub struct Instrumentation { +#[table_name = "instrumentations"] +struct InstrumentationRow { pub id: i64, pub work: i64, pub instrument: i64, } -/// A concrete work part that can be recorded. +/// Table data for a work part. #[derive(Insertable, Queryable, Debug, Clone)] -pub struct WorkPart { +#[table_name = "work_parts"] +struct WorkPartRow { pub id: i64, pub work: i64, pub part_index: i64, @@ -33,275 +64,194 @@ pub struct WorkPart { pub composer: Option, } -/// A heading between work parts. +/// Table data for a work section. +#[table_name = "work_sections"] #[derive(Insertable, Queryable, Debug, Clone)] -pub struct WorkSection { +struct WorkSectionRow { pub id: i64, pub work: i64, pub title: String, pub before_index: i64, } -/// A structure for collecting all available information on a work part. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkPartDescription { - pub title: String, - pub composer: Option, -} -/// A structure for collecting all available information on a work section. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkSectionDescription { - pub title: String, - pub before_index: i64, -} - -/// A structure for collecting all available information on a work. -#[derive(Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkDescription { - pub id: i64, - pub title: String, - pub composer: Person, - pub instruments: Vec, - pub parts: Vec, - pub sections: Vec, -} - -/// A structure representing data on a work part. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkPartInsertion { - pub title: String, - pub composer: Option, -} - -/// A structure representing data on a work section. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkSectionInsertion { - pub title: String, - pub before_index: i64, -} - -/// A structure representing data on a work. -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WorkInsertion { - pub composer: i64, - pub title: String, - pub instruments: Vec, - pub parts: Vec, - pub sections: Vec, -} - -/// Insert a new work. -pub fn insert_work(conn: &DbConn, id: u32, data: &WorkInsertion, created_by: &str) -> Result<()> { +/// Update an existing work or insert a new one. This will only succeed, if the user is allowed to +/// do that. +// TODO: Also add newly created associated items. +pub fn update_work(conn: &DbConn, work: &Work, user: &User) -> Result<()> { conn.transaction::<(), Error, _>(|| { - let id = id as i64; + let old_row = get_work_row(conn, work.id)?; - diesel::insert_into(works::table) - .values(Work { + let allowed = match old_row { + Some(row) => user.may_edit(&row.created_by), + None => user.may_create(), + }; + + if allowed { + let id = work.id as i64; + + // This will also delete rows from associated tables. + diesel::delete(works::table) + .filter(works::id.eq(id)) + .execute(conn)?; + + let row = WorkRow { id, - composer: data.composer.clone(), - title: data.title.clone(), - created_by: created_by.to_string(), - }) - .execute(conn)?; + composer: work.composer.id as i64, + title: work.title.clone(), + created_by: user.username.clone(), + }; - insert_work_data(conn, id, data)?; + diesel::insert_into(works::table) + .values(row) + .execute(conn)?; - Ok(()) + for instrument in &work.instruments { + diesel::insert_into(instrumentations::table) + .values(InstrumentationRow { + id: rand::random(), + work: id, + instrument: instrument.id as i64, + }) + .execute(conn)?; + } + + for (index, part) in work.parts.iter().enumerate() { + let row = WorkPartRow { + id: rand::random(), + work: id, + part_index: index.try_into()?, + title: part.title.clone(), + composer: part.composer.as_ref().map(|person| person.id as i64), + }; + + diesel::insert_into(work_parts::table) + .values(row) + .execute(conn)?; + } + + for section in &work.sections { + let row = WorkSectionRow { + id: rand::random(), + work: id, + title: section.title.clone(), + before_index: section.before_index, + }; + + diesel::insert_into(work_sections::table) + .values(row) + .execute(conn)?; + } + + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } })?; Ok(()) } -/// Update an existing work. -pub fn update_work(conn: &DbConn, id: u32, data: &WorkInsertion) -> Result<()> { - conn.transaction::<(), Error, _>(|| { - let id = id as i64; +/// Get an existing work and all available information from related tables. +pub fn get_work(conn: &DbConn, id: u32) -> Result> { + let work = match get_work_row(conn, id)? { + Some(row) => Some(get_description_for_work_row(conn, &row)?), + None => None, + }; - diesel::delete(instrumentations::table) - .filter(instrumentations::work.eq(id)) - .execute(conn)?; - - diesel::delete(work_parts::table) - .filter(work_parts::work.eq(id)) - .execute(conn)?; - - diesel::delete(work_sections::table) - .filter(work_sections::work.eq(id)) - .execute(conn)?; - - diesel::update(works::table) - .filter(works::id.eq(id)) - .set(( - works::composer.eq(data.composer), - works::title.eq(data.title.clone()), - )) - .execute(conn)?; - - insert_work_data(conn, id, data)?; - - Ok(()) - })?; - - Ok(()) + Ok(work) } -/// Helper method to populate tables related to a work. -fn insert_work_data(conn: &DbConn, id: i64, data: &WorkInsertion) -> Result<()> { - for instrument in &data.instruments { - diesel::insert_into(instrumentations::table) - .values(Instrumentation { - id: rand::random(), - work: id, - instrument: *instrument, - }) - .execute(conn)?; +/// Delete an existing work. This will fail if there are still other tables that relate to +/// this work except for the things that are part of the information on the work itself. Also, +/// this will only succeed, if the provided user is allowed to delete the work. +pub fn delete_work(conn: &DbConn, id: u32, user: &User) -> Result<()> { + if user.may_delete() { + diesel::delete(works::table.filter(works::id.eq(id as i64))).execute(conn)?; + Ok(()) + } else { + Err(Error::new(ServerError::Forbidden)) + } +} + +/// Get all existing works by a composer and related information from other tables. +pub fn get_works(conn: &DbConn, composer_id: u32) -> Result> { + let mut works: Vec = Vec::new(); + + let rows = works::table + .filter(works::composer.eq(composer_id as i64)) + .load::(conn)?; + + for row in rows { + works.push(get_description_for_work_row(conn, &row)?); } - for (index, part) in data.parts.iter().enumerate() { - let part = WorkPart { - id: rand::random(), - work: id, - part_index: index.try_into()?, - title: part.title.clone(), - composer: part.composer, - }; - - diesel::insert_into(work_parts::table) - .values(part) - .execute(conn)?; - } - - for section in &data.sections { - let section = WorkSection { - id: rand::random(), - work: id, - title: section.title.clone(), - before_index: section.before_index, - }; - - diesel::insert_into(work_sections::table) - .values(section) - .execute(conn)?; - } - - Ok(()) + Ok(works) } /// Get an already existing work without related rows from other tables. -fn get_work(conn: &DbConn, id: u32) -> Result> { +fn get_work_row(conn: &DbConn, id: u32) -> Result> { Ok(works::table .filter(works::id.eq(id as i64)) - .load::(conn)? - .first() - .cloned()) + .load::(conn)? + .into_iter() + .next()) } /// Retrieve all available information on a work from related tables. -fn get_description_for_work(conn: &DbConn, work: &Work) -> Result { +fn get_description_for_work_row(conn: &DbConn, row: &WorkRow) -> Result { let mut instruments: Vec = Vec::new(); let instrumentations = instrumentations::table - .filter(instrumentations::work.eq(work.id)) - .load::(conn)?; + .filter(instrumentations::work.eq(row.id)) + .load::(conn)?; for instrumentation in instrumentations { - instruments.push( - instruments::table - .filter(instruments::id.eq(instrumentation.instrument)) - .load::(conn)? - .first() - .cloned() - .ok_or(anyhow!( - "No instrument with ID: {}", - instrumentation.instrument - ))?, - ); + let id = instrumentation.instrument as u32; + instruments + .push(get_instrument(conn, id)?.ok_or(anyhow!("No instrument with ID: {}", id))?); } - let mut part_descriptions: Vec = Vec::new(); + let mut parts: Vec = Vec::new(); - let work_parts = work_parts::table - .filter(work_parts::work.eq(work.id)) - .load::(conn)?; + let part_rows = work_parts::table + .filter(work_parts::work.eq(row.id)) + .load::(conn)?; - for work_part in work_parts { - part_descriptions.push(WorkPartDescription { - title: work_part.title, - composer: match work_part.composer { - Some(composer) => Some( - persons::table - .filter(persons::id.eq(composer)) - .load::(conn)? - .first() - .cloned() - .ok_or(anyhow!("No person with ID: {}", composer))?, - ), + for part_row in part_rows { + parts.push(WorkPart { + title: part_row.title, + composer: match part_row.composer { + Some(id) => { + let id = id as u32; + Some(get_person(conn, id)?.ok_or(anyhow!("No person with ID: {}", id))?) + } None => None, }, }); } - let mut section_descriptions: Vec = Vec::new(); + let mut sections: Vec = Vec::new(); - let sections = work_sections::table - .filter(work_sections::work.eq(work.id)) - .load::(conn)?; + let section_rows = work_sections::table + .filter(work_sections::work.eq(row.id)) + .load::(conn)?; - for section in sections { - section_descriptions.push(WorkSectionDescription { + for section in section_rows { + sections.push(WorkSection { title: section.title, before_index: section.before_index, }); } - let person_id = work.composer.try_into()?; - let person = - get_person(conn, person_id)?.ok_or(anyhow!("Person doesn't exist: {}", person_id))?; + let id = row.composer as u32; + let composer = get_person(conn, id)?.ok_or(anyhow!("No person with ID: {}", id))?; - Ok(WorkDescription { - id: work.id, - composer: person, - title: work.title.clone(), + Ok(Work { + id: row.id as u32, + composer, + title: row.title.clone(), instruments, - parts: part_descriptions, - sections: section_descriptions, + parts, + sections, }) } - -/// Get an existing work and all available information from related tables. -pub fn get_work_description(conn: &DbConn, id: u32) -> Result> { - let work_description = match get_work(conn, id)? { - Some(work) => Some(get_description_for_work(conn, &work)?), - None => None, - }; - - Ok(work_description) -} - -/// Delete an existing work. This will fail if there are still other tables that relate to -/// this work except for the things that are part of the information on the work it -pub fn delete_work(conn: &DbConn, id: u32) -> Result<()> { - diesel::delete(works::table.filter(works::id.eq(id as i64))).execute(conn)?; - Ok(()) -} - -/// Get all existing works by a composer and related information from other tables. -pub fn get_work_descriptions(conn: &DbConn, composer_id: u32) -> Result> { - let mut work_descriptions: Vec = Vec::new(); - - let works = works::table - .filter(works::composer.eq(composer_id as i64)) - .load::(conn)?; - - for work in works { - work_descriptions.push(get_description_for_work(conn, &work)?); - } - - Ok(work_descriptions) -} diff --git a/musicus_server/src/routes/error.rs b/musicus_server/src/error.rs similarity index 88% rename from musicus_server/src/routes/error.rs rename to musicus_server/src/error.rs index 7a7019a..089aba8 100644 --- a/musicus_server/src/routes/error.rs +++ b/musicus_server/src/error.rs @@ -26,14 +26,17 @@ impl error::ResponseError for ServerError { } impl From for ServerError { - fn from(error: r2d2::Error) -> Self { + fn from(_: r2d2::Error) -> Self { ServerError::Internal } } impl From for ServerError { fn from(error: anyhow::Error) -> Self { - ServerError::Internal + match error.downcast() { + Ok(error) => error, + Err(_) => ServerError::Internal, + } } } diff --git a/musicus_server/src/main.rs b/musicus_server/src/main.rs index d77d001..3155f73 100644 --- a/musicus_server/src/main.rs +++ b/musicus_server/src/main.rs @@ -5,6 +5,7 @@ extern crate diesel; use actix_web::{App, HttpServer}; mod database; +mod error; mod routes; use routes::*; @@ -27,9 +28,9 @@ async fn main() -> std::io::Result<()> { .service(put_user) .service(get_user) .service(get_person) - .service(post_person) - .service(put_person) + .service(update_person) .service(get_persons) + .service(delete_person) }); server.bind("127.0.0.1:8087")?.run().await diff --git a/musicus_server/src/routes/auth.rs b/musicus_server/src/routes/auth.rs index e951f3d..fa697ad 100644 --- a/musicus_server/src/routes/auth.rs +++ b/musicus_server/src/routes/auth.rs @@ -1,6 +1,6 @@ -use super::ServerError; use crate::database; use crate::database::{DbConn, DbPool, User, UserInsertion}; +use crate::error::ServerError; use actix_web::{get, post, put, web, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; use anyhow::{anyhow, Result}; @@ -167,21 +167,6 @@ pub fn authenticate(conn: &DbConn, token: &str) -> Result { database::get_user(conn, &username)?.ok_or(anyhow!("User doesn't exist: {}", &username)) } -/// Check whether the user is allowed to create a new item. -pub fn may_create(user: &User) -> bool { - !user.is_banned -} - -/// Check whether the user is allowed to edit an item created by him or somebody else. -pub fn may_edit(user: &User, created_by: &str) -> bool { - !user.is_banned && (user.username == created_by || user.is_editor) -} - -/// Check whether the user is allowed to delete an item. -pub fn may_delete(user: &User) -> bool { - !user.is_banned && user.is_editor -} - /// Return a hash for a password that can be stored in the database. fn hash_password(password: &str) -> Result { let hash = argon2id13::pwhash( diff --git a/musicus_server/src/routes/mod.rs b/musicus_server/src/routes/mod.rs index 4caf6ef..c35ce5a 100644 --- a/musicus_server/src/routes/mod.rs +++ b/musicus_server/src/routes/mod.rs @@ -4,9 +4,6 @@ pub use auth::*; pub mod ensembles; pub use ensembles::*; -pub mod error; -pub use error::*; - pub mod instruments; pub use instruments::*; diff --git a/musicus_server/src/routes/persons.rs b/musicus_server/src/routes/persons.rs index fcc6ae6..1c98d77 100644 --- a/musicus_server/src/routes/persons.rs +++ b/musicus_server/src/routes/persons.rs @@ -1,7 +1,8 @@ -use super::{authenticate, may_create, may_delete, may_edit, ServerError}; +use super::authenticate; use crate::database; -use crate::database::{DbPool, PersonInsertion}; -use actix_web::{delete, get, post, put, web, HttpResponse}; +use crate::database::{DbPool, Person}; +use crate::error::ServerError; +use actix_web::{delete, get, post, web, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; /// Get an existing person. @@ -10,60 +11,29 @@ pub async fn get_person( db: web::Data, id: web::Path, ) -> Result { - let person = web::block(move || { + let data = web::block(move || { let conn = db.into_inner().get()?; database::get_person(&conn, id.into_inner())?.ok_or(ServerError::NotFound) }) .await?; - Ok(HttpResponse::Ok().json(person)) + Ok(HttpResponse::Ok().json(data)) } -/// Add a new person. The user must be authorized to do that. +/// Add a new person or update an existin one. The user must be authorized to do that. #[post("/persons")] -pub async fn post_person( +pub async fn update_person( auth: BearerAuth, db: web::Data, - data: web::Json, -) -> Result { - let id = rand::random(); - - web::block(move || { - let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - if may_create(&user) { - database::insert_person(&conn, id, &data.into_inner(), &user.username)?; - Ok(()) - } else { - Err(ServerError::Forbidden) - } - }) - .await?; - - Ok(HttpResponse::Ok().body(id.to_string())) -} - -#[put("/persons/{id}")] -pub async fn put_person( - auth: BearerAuth, - db: web::Data, - id: web::Path, - data: web::Json, + data: web::Json, ) -> Result { web::block(move || { let conn = db.into_inner().get()?; - let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - let id = id.into_inner(); - let old_person = database::get_person(&conn, id)?.ok_or(ServerError::NotFound)?; + database::update_person(&conn, &data.into_inner(), &user)?; - if may_edit(&user, &old_person.created_by) { - database::update_person(&conn, id, &data.into_inner())?; - Ok(()) - } else { - Err(ServerError::Forbidden) - } + Ok(()) }) .await?; @@ -72,13 +42,13 @@ pub async fn put_person( #[get("/persons")] pub async fn get_persons(db: web::Data) -> Result { - let persons = web::block(move || { + let data = web::block(move || { let conn = db.into_inner().get()?; Ok(database::get_persons(&conn)?) }) .await?; - Ok(HttpResponse::Ok().json(persons)) + Ok(HttpResponse::Ok().json(data)) } #[delete("/persons/{id}")] @@ -91,12 +61,9 @@ pub async fn delete_person( let conn = db.into_inner().get()?; let user = authenticate(&conn, auth.token()).or(Err(ServerError::Unauthorized))?; - if may_delete(&user) { - database::delete_person(&conn, id.into_inner())?; - Ok(()) - } else { - Err(ServerError::Forbidden) - } + database::delete_person(&conn, id.into_inner(), &user)?; + + Ok(()) }) .await?;