diff options
Diffstat (limited to 'rust/src')
-rw-r--r-- | rust/src/api_helpers.rs | 44 | ||||
-rw-r--r-- | rust/src/api_server.rs | 858 | ||||
-rw-r--r-- | rust/src/api_wrappers.rs | 117 | ||||
-rw-r--r-- | rust/src/database_entity_crud.rs | 1005 | ||||
-rw-r--r-- | rust/src/database_models.rs | 121 | ||||
-rw-r--r-- | rust/src/lib.rs | 5 |
6 files changed, 1502 insertions, 648 deletions
diff --git a/rust/src/api_helpers.rs b/rust/src/api_helpers.rs index 489631b3..925a6073 100644 --- a/rust/src/api_helpers.rs +++ b/rust/src/api_helpers.rs @@ -5,8 +5,12 @@ use diesel; use diesel::prelude::*; use errors::*; use regex::Regex; +use std::str::FromStr; use uuid::Uuid; +pub type DbConn = + diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; + /// This function should always be run within a transaction pub fn get_or_create_editgroup(editor_id: Uuid, conn: &PgConnection) -> Result<Uuid> { // check for current active @@ -34,10 +38,7 @@ pub fn accept_editgroup(editgroup_id: Uuid, conn: &PgConnection) -> Result<Chang .count() .get_result(conn)?; if count > 0 { - bail!( - "editgroup {} has already been accepted", - editgroup_id.to_string() - ); + return Err(ErrorKind::EditgroupAlreadyAccepted(uuid2fcid(&editgroup_id)).into()); } // for each entity type... @@ -90,6 +91,31 @@ pub fn accept_editgroup(editgroup_id: Uuid, conn: &PgConnection) -> Result<Chang Ok(entry) } +#[derive(Clone)] +pub struct FatCatId(Uuid); + +impl ToString for FatCatId { + fn to_string(&self) -> String { + uuid2fcid(&self.to_uuid()) + } +} + +impl FromStr for FatCatId { + type Err = Error; + fn from_str(s: &str) -> Result<FatCatId> { + fcid2uuid(s).map(|u| FatCatId(u)) + } +} + +impl FatCatId { + pub fn to_uuid(&self) -> Uuid { + self.0 + } + pub fn from_uuid(u: &Uuid) -> FatCatId { + FatCatId(u.clone()) + } +} + /// Convert fatcat IDs (base32 strings) to UUID pub fn fcid2uuid(fcid: &str) -> Result<Uuid> { if fcid.len() != 26 { @@ -181,7 +207,7 @@ pub fn check_issn(raw: &str) -> Result<()> { pub fn check_orcid(raw: &str) -> Result<()> { lazy_static! { - static ref RE: Regex = Regex::new(r"^\d{4}-\d{4}-\d{4}-\d{4}$").unwrap(); + static ref RE: Regex = Regex::new(r"^\d{4}-\d{4}-\d{4}-\d{3}[\dX]$").unwrap(); } if RE.is_match(raw) { Ok(()) @@ -193,6 +219,14 @@ pub fn check_orcid(raw: &str) -> Result<()> { } } +#[test] +fn test_check_orcid() { + assert!(check_orcid("0123-4567-3456-6789").is_ok()); + assert!(check_orcid("0123-4567-3456-678X").is_ok()); + assert!(check_orcid("01234567-3456-6780").is_err()); + assert!(check_orcid("0x23-4567-3456-6780").is_err()); +} + // TODO: make the above checks "more correct" // TODO: check ISBN-13 // TODO: check hashes (SHA-1, etc) diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index b445d63a..31b71395 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -2,6 +2,7 @@ use api_helpers::*; use chrono; +use database_entity_crud::{EditContext, EntityCrud}; use database_models::*; use database_schema::{ abstracts, changelog, container_edit, container_ident, container_rev, creator_edit, @@ -14,54 +15,34 @@ use diesel::{self, insert_into}; use errors::*; use fatcat_api::models; use fatcat_api::models::*; -use sha1::Sha1; +use std::str::FromStr; use uuid::Uuid; use ConnectionPool; -type DbConn = diesel::r2d2::PooledConnection<diesel::r2d2::ConnectionManager<diesel::PgConnection>>; - macro_rules! entity_batch_handler { - ($post_handler:ident, $post_batch_handler:ident, $model:ident) => { + ($post_batch_handler:ident, $model:ident) => { pub fn $post_batch_handler( &self, entity_list: &[models::$model], + autoaccept: bool, + editgroup: Option<String>, conn: &DbConn, ) -> Result<Vec<EntityEdit>> { - let mut ret: Vec<EntityEdit> = vec![]; - for entity in entity_list { - ret.push(self.$post_handler(entity.clone(), conn)?); - } - Ok(ret) - } - } -} - -macro_rules! entity_history_handler { - ($history_handler:ident, $edit_row_type:ident, $edit_table:ident) => { - pub fn $history_handler( - &self, - id: &Uuid, - limit: Option<i64>, - conn: &DbConn, - ) -> Result<Vec<EntityHistoryEntry>> { - let limit = limit.unwrap_or(50); - - let rows: Vec<(EditgroupRow, ChangelogRow, $edit_row_type)> = editgroup::table - .inner_join(changelog::table) - .inner_join($edit_table::table) - .filter($edit_table::ident_id.eq(id)) - .order(changelog::id.desc()) - .limit(limit) - .get_results(conn)?; - let history: Vec<EntityHistoryEntry> = rows.into_iter() - .map(|(eg_row, cl_row, e_row)| EntityHistoryEntry { - edit: e_row.into_model().expect("edit row to model"), - editgroup: eg_row.into_model_partial(), - changelog_entry: cl_row.into_model(), - }) - .collect(); - Ok(history) + let editgroup_id: Option<FatCatId> = match editgroup { + Some(s) => Some(FatCatId::from_str(&s)?), + None => None, + }; + let edit_context = make_edit_context(conn, editgroup_id.clone(), autoaccept)?; + let model_list: Vec<&models::$model> = entity_list.iter().map(|e| e).collect(); + let edits = $model::db_create_batch(conn, &edit_context, model_list.as_slice())?; + + if autoaccept { + let _clr: ChangelogRow = diesel::insert_into(changelog::table) + .values((changelog::editgroup_id.eq(edit_context.editgroup_id.to_uuid()),)) + .get_result(conn)?; + } + edits.into_iter().map(|e| e.into_model()).collect() } } } @@ -77,224 +58,30 @@ macro_rules! count_entity { }}; } -#[derive(Clone)] -pub struct Server { - pub db_pool: ConnectionPool, -} - -fn container_row2entity( - ident: Option<ContainerIdentRow>, - rev: ContainerRevRow, -) -> Result<ContainerEntity> { - let (state, ident_id, redirect_id) = match ident { - Some(i) => ( - Some(i.state().unwrap().shortname()), - Some(uuid2fcid(&i.id)), - i.redirect_id.map(|u| uuid2fcid(&u)), - ), - None => (None, None, None), +fn make_edit_context(conn: &DbConn, editgroup_id: Option<FatCatId>, autoaccept: bool) -> Result<EditContext> { + let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth + let editgroup_id: FatCatId = match (editgroup_id, autoaccept) { + (Some(eg), _) => eg, + // If autoaccept and no editgroup_id passed, always create a new one for this transaction + (None, true) => { + let eg_row: EditgroupRow = diesel::insert_into(editgroup::table) + .values((editgroup::editor_id.eq(editor_id),)) + .get_result(conn)?; + FatCatId::from_uuid(&eg_row.id) + }, + (None, false) => FatCatId::from_uuid(&get_or_create_editgroup(editor_id, conn)?), }; - Ok(ContainerEntity { - issnl: rev.issnl, - wikidata_qid: rev.wikidata_qid, - publisher: rev.publisher, - name: rev.name, - abbrev: rev.abbrev, - coden: rev.coden, - state: state, - ident: ident_id, - revision: Some(rev.id.to_string()), - redirect: redirect_id, - extra: rev.extra_json, - editgroup_id: None, - }) -} - -fn creator_row2entity(ident: Option<CreatorIdentRow>, rev: CreatorRevRow) -> Result<CreatorEntity> { - let (state, ident_id, redirect_id) = match ident { - Some(i) => ( - Some(i.state().unwrap().shortname()), - Some(uuid2fcid(&i.id)), - i.redirect_id.map(|u| uuid2fcid(&u)), - ), - None => (None, None, None), - }; - Ok(CreatorEntity { - display_name: rev.display_name, - given_name: rev.given_name, - surname: rev.surname, - orcid: rev.orcid, - wikidata_qid: rev.wikidata_qid, - state: state, - ident: ident_id, - revision: Some(rev.id.to_string()), - redirect: redirect_id, - editgroup_id: None, - extra: rev.extra_json, - }) -} - -fn file_row2entity( - ident: Option<FileIdentRow>, - rev: FileRevRow, - conn: &DbConn, -) -> Result<FileEntity> { - let (state, ident_id, redirect_id) = match ident { - Some(i) => ( - Some(i.state().unwrap().shortname()), - Some(uuid2fcid(&i.id)), - i.redirect_id.map(|u| uuid2fcid(&u)), - ), - None => (None, None, None), - }; - - let releases: Vec<String> = file_release::table - .filter(file_release::file_rev.eq(rev.id)) - .get_results(conn)? - .into_iter() - .map(|r: FileReleaseRow| uuid2fcid(&r.target_release_ident_id)) - .collect(); - - let urls: Vec<FileEntityUrls> = file_rev_url::table - .filter(file_rev_url::file_rev.eq(rev.id)) - .get_results(conn)? - .into_iter() - .map(|r: FileRevUrlRow| FileEntityUrls { - rel: r.rel, - url: r.url, - }) - .collect(); - - Ok(FileEntity { - sha1: rev.sha1, - sha256: rev.sha256, - md5: rev.md5, - size: rev.size.map(|v| v as i64), - urls: Some(urls), - mimetype: rev.mimetype, - releases: Some(releases), - state: state, - ident: ident_id, - revision: Some(rev.id.to_string()), - redirect: redirect_id, - editgroup_id: None, - extra: rev.extra_json, + Ok(EditContext { + editor_id: FatCatId::from_uuid(&editor_id), + editgroup_id: editgroup_id, + extra_json: None, + autoaccept: autoaccept, }) } -fn release_row2entity( - ident: Option<ReleaseIdentRow>, - rev: ReleaseRevRow, - conn: &DbConn, -) -> Result<ReleaseEntity> { - let (state, ident_id, redirect_id) = match ident { - Some(i) => ( - Some(i.state().unwrap().shortname()), - Some(uuid2fcid(&i.id)), - i.redirect_id.map(|u| uuid2fcid(&u)), - ), - None => (None, None, None), - }; - - let refs: Vec<ReleaseRef> = release_ref::table - .filter(release_ref::release_rev.eq(rev.id)) - .order(release_ref::index_val.asc()) - .get_results(conn) - .expect("fetch release refs") - .into_iter() - .map(|r: ReleaseRefRow| ReleaseRef { - index: r.index_val, - key: r.key, - extra: r.extra_json, - container_title: r.container_title, - year: r.year, - title: r.title, - locator: r.locator, - target_release_id: r.target_release_ident_id.map(|v| uuid2fcid(&v)), - }) - .collect(); - - let contribs: Vec<ReleaseContrib> = release_contrib::table - .filter(release_contrib::release_rev.eq(rev.id)) - .order((release_contrib::role.asc(), release_contrib::index_val.asc())) - .get_results(conn) - .expect("fetch release refs") - .into_iter() - .map(|c: ReleaseContribRow| ReleaseContrib { - index: c.index_val, - raw_name: c.raw_name, - role: c.role, - extra: c.extra_json, - creator_id: c.creator_ident_id.map(|v| uuid2fcid(&v)), - creator: None, - }) - .collect(); - - let abstracts: Vec<ReleaseEntityAbstracts> = release_rev_abstract::table - .inner_join(abstracts::table) - .filter(release_rev_abstract::release_rev.eq(rev.id)) - .get_results(conn)? - .into_iter() - .map( - |r: (ReleaseRevAbstractRow, AbstractsRow)| ReleaseEntityAbstracts { - sha1: Some(r.0.abstract_sha1), - mimetype: r.0.mimetype, - lang: r.0.lang, - content: Some(r.1.content), - }, - ) - .collect(); - - Ok(ReleaseEntity { - title: rev.title, - release_type: rev.release_type, - release_status: rev.release_status, - release_date: rev.release_date - .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), - doi: rev.doi, - pmid: rev.pmid, - pmcid: rev.pmcid, - isbn13: rev.isbn13, - core_id: rev.core_id, - wikidata_qid: rev.wikidata_qid, - volume: rev.volume, - issue: rev.issue, - pages: rev.pages, - files: None, - container: None, - container_id: rev.container_ident_id.map(|u| uuid2fcid(&u)), - publisher: rev.publisher, - language: rev.language, - work_id: Some(uuid2fcid(&rev.work_ident_id)), - refs: Some(refs), - contribs: Some(contribs), - abstracts: Some(abstracts), - state: state, - ident: ident_id, - revision: Some(rev.id.to_string()), - redirect: redirect_id, - editgroup_id: None, - extra: rev.extra_json, - }) -} - -fn work_row2entity(ident: Option<WorkIdentRow>, rev: WorkRevRow) -> Result<WorkEntity> { - let (state, ident_id, redirect_id) = match ident { - Some(i) => ( - Some(i.state().unwrap().shortname()), - Some(uuid2fcid(&i.id)), - i.redirect_id.map(|u| uuid2fcid(&u)), - ), - None => (None, None, None), - }; - Ok(WorkEntity { - state: state, - ident: ident_id, - revision: Some(rev.id.to_string()), - redirect: redirect_id, - editgroup_id: None, - extra: rev.extra_json, - }) +#[derive(Clone)] +pub struct Server { + pub db_pool: ConnectionPool, } impl Server { @@ -304,13 +91,7 @@ impl Server { _expand: Option<String>, conn: &DbConn, ) -> Result<ContainerEntity> { - // TODO: handle Deletions - let (ident, rev): (ContainerIdentRow, ContainerRevRow) = container_ident::table - .find(id) - .inner_join(container_rev::table) - .first(conn)?; - - container_row2entity(Some(ident), rev) + ContainerEntity::db_get(conn, FatCatId::from_uuid(id)) } pub fn lookup_container_handler(&self, issnl: &str, conn: &DbConn) -> Result<ContainerEntity> { @@ -318,11 +99,14 @@ impl Server { let (ident, rev): (ContainerIdentRow, ContainerRevRow) = container_ident::table .inner_join(container_rev::table) .filter(container_rev::issnl.eq(issnl)) + // This NOT NULL is here to ensure the postgresql query planner that it can use an + // index + .filter(container_rev::issnl.is_not_null()) .filter(container_ident::is_live.eq(true)) .filter(container_ident::redirect_id.is_null()) .first(conn)?; - container_row2entity(Some(ident), rev) + ContainerEntity::db_from_row(conn, rev, Some(ident)) } pub fn get_creator_handler( @@ -331,12 +115,7 @@ impl Server { _expand: Option<String>, conn: &DbConn, ) -> Result<CreatorEntity> { - let (ident, rev): (CreatorIdentRow, CreatorRevRow) = creator_ident::table - .find(id) - .inner_join(creator_rev::table) - .first(conn)?; - - creator_row2entity(Some(ident), rev) + CreatorEntity::db_get(conn, FatCatId::from_uuid(id)) } pub fn lookup_creator_handler(&self, orcid: &str, conn: &DbConn) -> Result<CreatorEntity> { @@ -344,11 +123,14 @@ impl Server { let (ident, rev): (CreatorIdentRow, CreatorRevRow) = creator_ident::table .inner_join(creator_rev::table) .filter(creator_rev::orcid.eq(orcid)) + // This NOT NULL is here to ensure the postgresql query planner that it can use an + // index + .filter(creator_rev::orcid.is_not_null()) .filter(creator_ident::is_live.eq(true)) .filter(creator_ident::redirect_id.is_null()) .first(conn)?; - creator_row2entity(Some(ident), rev) + CreatorEntity::db_from_row(conn, rev, Some(ident)) } pub fn get_creator_releases_handler( @@ -367,8 +149,9 @@ impl Server { .filter(release_ident::redirect_id.is_null()) .load(conn)?; + // TODO: from_rows, not from_row? rows.into_iter() - .map(|(rev, ident, _)| release_row2entity(Some(ident), rev, conn)) + .map(|(rev, ident, _)| ReleaseEntity::db_from_row(conn, rev, Some(ident))) .collect() } @@ -378,23 +161,21 @@ impl Server { _expand: Option<String>, conn: &DbConn, ) -> Result<FileEntity> { - let (ident, rev): (FileIdentRow, FileRevRow) = file_ident::table - .find(id) - .inner_join(file_rev::table) - .first(conn)?; - - file_row2entity(Some(ident), rev, conn) + FileEntity::db_get(conn, FatCatId::from_uuid(id)) } pub fn lookup_file_handler(&self, sha1: &str, conn: &DbConn) -> Result<FileEntity> { let (ident, rev): (FileIdentRow, FileRevRow) = file_ident::table .inner_join(file_rev::table) .filter(file_rev::sha1.eq(sha1)) + // This NOT NULL is here to ensure the postgresql query planner that it can use an + // index + .filter(file_rev::sha1.is_not_null()) .filter(file_ident::is_live.eq(true)) .filter(file_ident::redirect_id.is_null()) .first(conn)?; - file_row2entity(Some(ident), rev, conn) + FileEntity::db_from_row(conn, rev, Some(ident)) } pub fn get_release_handler( @@ -403,12 +184,7 @@ impl Server { expand: Option<String>, conn: &DbConn, ) -> Result<ReleaseEntity> { - let (ident, rev): (ReleaseIdentRow, ReleaseRevRow) = release_ident::table - .find(id) - .inner_join(release_rev::table) - .first(conn)?; - - let mut release = release_row2entity(Some(ident), rev, conn)?; + let mut release = ReleaseEntity::db_get(conn, FatCatId::from_uuid(id))?; // For now, if there is any expand param we do them all if expand.is_some() { @@ -419,7 +195,6 @@ impl Server { Some(self.get_container_handler(&fcid2uuid(&cid)?, None, conn)?); } } - Ok(release) } @@ -428,26 +203,29 @@ impl Server { let (ident, rev): (ReleaseIdentRow, ReleaseRevRow) = release_ident::table .inner_join(release_rev::table) .filter(release_rev::doi.eq(doi)) + // This NOT NULL is here to ensure the postgresql query planner that it can use an + // index + .filter(release_rev::doi.is_not_null()) .filter(release_ident::is_live.eq(true)) .filter(release_ident::redirect_id.is_null()) .first(conn)?; - release_row2entity(Some(ident), rev, conn) + ReleaseEntity::db_from_row(conn, rev, Some(ident)) } pub fn get_release_files_handler(&self, id: &str, conn: &DbConn) -> Result<Vec<FileEntity>> { - let id = fcid2uuid(&id)?; + let ident = FatCatId::from_str(id)?; let rows: Vec<(FileRevRow, FileIdentRow, FileReleaseRow)> = file_rev::table .inner_join(file_ident::table) .inner_join(file_release::table) - .filter(file_release::target_release_ident_id.eq(&id)) + .filter(file_release::target_release_ident_id.eq(&ident.to_uuid())) .filter(file_ident::is_live.eq(true)) .filter(file_ident::redirect_id.is_null()) .load(conn)?; rows.into_iter() - .map(|(rev, ident, _)| file_row2entity(Some(ident), rev, conn)) + .map(|(rev, ident, _)| FileEntity::db_from_row(conn, rev, Some(ident))) .collect() } @@ -457,12 +235,7 @@ impl Server { _expand: Option<String>, conn: &DbConn, ) -> Result<WorkEntity> { - let (ident, rev): (WorkIdentRow, WorkRevRow) = work_ident::table - .find(id) - .inner_join(work_rev::table) - .first(conn)?; - - work_row2entity(Some(ident), rev) + WorkEntity::db_get(conn, FatCatId::from_uuid(id)) } pub fn get_work_releases_handler(&self, id: &str, conn: &DbConn) -> Result<Vec<ReleaseEntity>> { @@ -476,7 +249,7 @@ impl Server { .load(conn)?; rows.into_iter() - .map(|(rev, ident)| release_row2entity(Some(ident), rev, conn)) + .map(|(rev, ident)| ReleaseEntity::db_from_row(conn, rev, Some(ident))) .collect() } @@ -485,37 +258,29 @@ impl Server { entity: models::ContainerEntity, conn: &DbConn, ) -> Result<EntityEdit> { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth - let editgroup_id: Uuid = match entity.editgroup_id { - None => get_or_create_editgroup(editor_id, conn)?, - Some(param) => fcid2uuid(¶m)?, - }; - if let Some(ref extid) = entity.wikidata_qid { - check_wikidata_qid(extid)?; - } - if let Some(ref extid) = entity.issnl { - check_issn(extid)?; - } + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_create(conn, &edit_context)?; + edit.into_model() + } - let rev_id: Uuid = insert_into(container_rev::table) - .values((container_rev::name.eq(entity.name), - container_rev::publisher.eq(entity.publisher), - container_rev::issnl.eq(entity.issnl), - container_rev::wikidata_qid.eq(entity.wikidata_qid), - container_rev::abbrev.eq(entity.abbrev), - container_rev::coden.eq(entity.coden), - container_rev::extra_json.eq(entity.extra))) - .returning(container_rev::id) - .get_result(conn)?; - let ident_id: Uuid = insert_into(container_ident::table) - .values(container_ident::rev_id.eq(rev_id)) - .returning(container_ident::id) - .get_result(conn)?; - let edit: ContainerEditRow = insert_into(container_edit::table) - .values((container_edit::editgroup_id.eq(editgroup_id), - container_edit::ident_id.eq(ident_id), - container_edit::rev_id.eq(rev_id))) - .get_result(conn)?; + pub fn update_container_handler( + &self, + id: &Uuid, + entity: models::ContainerEntity, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; + edit.into_model() + } + pub fn delete_container_handler( + &self, + id: &Uuid, + editgroup_id: Option<Uuid>, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)), false)?; + let edit = ContainerEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; edit.into_model() } @@ -524,37 +289,29 @@ impl Server { entity: models::CreatorEntity, conn: &DbConn, ) -> Result<EntityEdit> { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth - let editgroup_id = match entity.editgroup_id { - None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), - Some(param) => fcid2uuid(¶m)?, - }; - if let Some(ref extid) = entity.orcid { - check_orcid(extid)?; - } - if let Some(ref extid) = entity.wikidata_qid { - check_wikidata_qid(extid)?; - } - - let rev_id: Uuid = insert_into(creator_rev::table) - .values((creator_rev::display_name.eq(entity.display_name), - creator_rev::given_name.eq(entity.given_name), - creator_rev::surname.eq(entity.surname), - creator_rev::orcid.eq(entity.orcid), - creator_rev::wikidata_qid.eq(entity.wikidata_qid), - creator_rev::extra_json.eq(entity.extra))) - .returning(creator_rev::id) - .get_result(conn)?; - let ident_id: Uuid = insert_into(creator_ident::table) - .values(creator_ident::rev_id.eq(rev_id)) - .returning(creator_ident::id) - .get_result(conn)?; - let edit: CreatorEditRow = insert_into(creator_edit::table) - .values((creator_edit::editgroup_id.eq(editgroup_id), - creator_edit::ident_id.eq(ident_id), - creator_edit::rev_id.eq(rev_id))) - .get_result(conn)?; + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_create(conn, &edit_context)?; + edit.into_model() + } + pub fn update_creator_handler( + &self, + id: &Uuid, + entity: models::CreatorEntity, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; + edit.into_model() + } + pub fn delete_creator_handler( + &self, + id: &Uuid, + editgroup_id: Option<Uuid>, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)), false)?; + let edit = CreatorEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; edit.into_model() } @@ -563,77 +320,29 @@ impl Server { entity: models::FileEntity, conn: &DbConn, ) -> Result<EntityEdit> { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth - let editgroup_id = match entity.editgroup_id { - None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), - Some(param) => fcid2uuid(¶m)?, - }; - - let rev_id: Uuid = insert_into(file_rev::table) - .values((file_rev::size.eq(entity.size), - file_rev::sha1.eq(entity.sha1), - file_rev::sha256.eq(entity.sha256), - file_rev::md5.eq(entity.md5), - file_rev::mimetype.eq(entity.mimetype), - file_rev::extra_json.eq(entity.extra))) - .returning(file_rev::id) - .get_result(conn)?; - let ident_id: Uuid = insert_into(file_ident::table) - .values(file_ident::rev_id.eq(rev_id)) - .returning(file_ident::id) - .get_result(conn)?; - let edit: FileEditRow = insert_into(file_edit::table) - .values((file_edit::editgroup_id.eq(editgroup_id), - file_edit::ident_id.eq(ident_id), - file_edit::rev_id.eq(rev_id))) - .get_result(conn)?; - - let _releases: Option<Vec<FileReleaseRow>> = match entity.releases { - None => None, - Some(release_list) => { - if release_list.is_empty() { - Some(vec![]) - } else { - let release_rows: Vec<FileReleaseRow> = release_list - .iter() - .map(|r| FileReleaseRow { - file_rev: edit.rev_id.unwrap(), - target_release_ident_id: fcid2uuid(r) - .expect("invalid fatcat identifier"), - }) - .collect(); - let release_rows: Vec<FileReleaseRow> = insert_into(file_release::table) - .values(release_rows) - .get_results(conn) - .expect("error inserting file_releases"); - Some(release_rows) - } - } - }; - - let _urls: Option<Vec<FileRevUrlRow>> = match entity.urls { - None => None, - Some(url_list) => { - if url_list.is_empty() { - Some(vec![]) - } else { - let url_rows: Vec<FileRevUrlNewRow> = url_list - .into_iter() - .map(|u| FileRevUrlNewRow { - file_rev: edit.rev_id.unwrap(), - rel: u.rel, - url: u.url, - }) - .collect(); - let url_rows: Vec<FileRevUrlRow> = insert_into(file_rev_url::table) - .values(url_rows) - .get_results(conn) - .expect("error inserting file_rev_url"); - Some(url_rows) - } - } - }; + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_create(conn, &edit_context)?; + edit.into_model() + } + pub fn update_file_handler( + &self, + id: &Uuid, + entity: models::FileEntity, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; + edit.into_model() + } + pub fn delete_file_handler( + &self, + id: &Uuid, + editgroup_id: Option<Uuid>, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)), false)?; + let edit = FileEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; edit.into_model() } @@ -642,172 +351,29 @@ impl Server { entity: models::ReleaseEntity, conn: &DbConn, ) -> Result<EntityEdit> { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth - let editgroup_id = match entity.editgroup_id { - None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), - Some(param) => fcid2uuid(¶m)?, - }; - if let Some(ref extid) = entity.doi { - check_doi(extid)?; - } - if let Some(ref extid) = entity.pmid { - check_pmid(extid)?; - } - if let Some(ref extid) = entity.pmcid { - check_pmcid(extid)?; - } - if let Some(ref extid) = entity.wikidata_qid { - check_wikidata_qid(extid)?; - } - - let work_id = match entity.work_id { - Some(work_id) => fcid2uuid(&work_id)?, - None => { - // If a work_id wasn't passed, create a new work under the current editgroup - let work_model = models::WorkEntity { - ident: None, - revision: None, - redirect: None, - state: None, - editgroup_id: Some(uuid2fcid(&editgroup_id)), - extra: None, - }; - let new_entity = self.create_work_handler(work_model, conn)?; - fcid2uuid(&new_entity.ident)? - } - }; - - let container_id: Option<Uuid> = match entity.container_id { - Some(id) => Some(fcid2uuid(&id)?), - None => None, - }; - - let rev_id: Uuid = insert_into(release_rev::table) - .values((release_rev::title.eq(entity.title), - release_rev::release_type.eq(entity.release_type), - release_rev::release_status.eq(entity.release_status), - release_rev::release_date.eq(entity.release_date.map(|v| v.naive_utc().date())), - release_rev::doi.eq(entity.doi), - release_rev::pmid.eq(entity.pmid), - release_rev::pmcid.eq(entity.pmcid), - release_rev::wikidata_qid.eq(entity.wikidata_qid), - release_rev::isbn13.eq(entity.isbn13), - release_rev::core_id.eq(entity.core_id), - release_rev::volume.eq(entity.volume), - release_rev::issue.eq(entity.issue), - release_rev::pages.eq(entity.pages), - release_rev::work_ident_id.eq(work_id), - release_rev::container_ident_id.eq(container_id), - release_rev::publisher.eq(entity.publisher), - release_rev::language.eq(entity.language), - release_rev::extra_json.eq(entity.extra))) - .returning(release_rev::id) - .get_result(conn)?; - let ident_id: Uuid = insert_into(release_ident::table) - .values(release_ident::rev_id.eq(rev_id)) - .returning(release_ident::id) - .get_result(conn)?; - let edit: ReleaseEditRow = insert_into(release_edit::table) - .values((release_edit::editgroup_id.eq(editgroup_id), - release_edit::ident_id.eq(ident_id), - release_edit::rev_id.eq(rev_id))) - .get_result(conn)?; - - let _refs: Option<Vec<ReleaseRefRow>> = match entity.refs { - None => None, - Some(ref_list) => { - if ref_list.is_empty() { - Some(vec![]) - } else { - let ref_rows: Vec<ReleaseRefNewRow> = ref_list - .iter() - .map(|r| ReleaseRefNewRow { - release_rev: edit.rev_id.unwrap(), - target_release_ident_id: r.target_release_id - .clone() - .map(|v| fcid2uuid(&v).expect("valid fatcat identifier")), - index_val: r.index, - key: r.key.clone(), - container_title: r.container_title.clone(), - year: r.year, - title: r.title.clone(), - locator: r.locator.clone(), - extra_json: r.extra.clone(), - }) - .collect(); - let ref_rows: Vec<ReleaseRefRow> = insert_into(release_ref::table) - .values(ref_rows) - .get_results(conn) - .expect("error inserting release_refs"); - Some(ref_rows) - } - } - }; - - let _contribs: Option<Vec<ReleaseContribRow>> = match entity.contribs { - None => None, - Some(contrib_list) => { - if contrib_list.is_empty() { - Some(vec![]) - } else { - let contrib_rows: Vec<ReleaseContribNewRow> = contrib_list - .iter() - .map(|c| ReleaseContribNewRow { - release_rev: edit.rev_id.unwrap(), - creator_ident_id: c.creator_id - .clone() - .map(|v| fcid2uuid(&v).expect("valid fatcat identifier")), - raw_name: c.raw_name.clone(), - index_val: c.index, - role: c.role.clone(), - extra_json: c.extra.clone(), - }) - .collect(); - let contrib_rows: Vec<ReleaseContribRow> = insert_into(release_contrib::table) - .values(contrib_rows) - .get_results(conn) - .expect("error inserting release_contribs"); - Some(contrib_rows) - } - } - }; - - if let Some(abstract_list) = entity.abstracts { - // For rows that specify content, we need to insert the abstract if it doesn't exist - // already - let new_abstracts: Vec<AbstractsRow> = abstract_list - .iter() - .filter(|ea| ea.content.is_some()) - .map(|c| AbstractsRow { - sha1: Sha1::from(c.content.clone().unwrap()).hexdigest(), - content: c.content.clone().unwrap(), - }) - .collect(); - if !new_abstracts.is_empty() { - // Sort of an "upsert"; only inserts new abstract rows if they don't already exist - insert_into(abstracts::table) - .values(&new_abstracts) - .on_conflict(abstracts::sha1) - .do_nothing() - .execute(conn)?; - } - let release_abstract_rows: Vec<ReleaseRevAbstractNewRow> = abstract_list - .into_iter() - .map(|c| ReleaseRevAbstractNewRow { - release_rev: edit.rev_id.unwrap(), - abstract_sha1: match c.content { - Some(ref content) => Sha1::from(content).hexdigest(), - None => c.sha1.expect("either abstract_sha1 or content is required"), - }, - lang: c.lang, - mimetype: c.mimetype, - }) - .collect(); - insert_into(release_rev_abstract::table) - .values(release_abstract_rows) - .execute(conn)?; - } + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_create(conn, &edit_context)?; + edit.into_model() + } + pub fn update_release_handler( + &self, + id: &Uuid, + entity: models::ReleaseEntity, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; + edit.into_model() + } + pub fn delete_release_handler( + &self, + id: &Uuid, + editgroup_id: Option<Uuid>, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)), false)?; + let edit = ReleaseEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; edit.into_model() } @@ -816,25 +382,31 @@ impl Server { entity: models::WorkEntity, conn: &DbConn, ) -> Result<EntityEdit> { - let editor_id = Uuid::parse_str("00000000-0000-0000-AAAA-000000000001")?; // TODO: auth - let editgroup_id = match entity.editgroup_id { - None => get_or_create_editgroup(editor_id, conn).expect("current editgroup"), - Some(param) => fcid2uuid(¶m)?, - }; + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_create(conn, &edit_context)?; + edit.into_model() + } + + pub fn update_work_handler( + &self, + id: &Uuid, + entity: models::WorkEntity, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?, false)?; + let edit = entity.db_update(conn, &edit_context, FatCatId::from_uuid(id))?; + edit.into_model() + } + + pub fn delete_work_handler( + &self, + id: &Uuid, + editgroup_id: Option<Uuid>, + conn: &DbConn, + ) -> Result<EntityEdit> { + let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)), false)?; + let edit = WorkEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; - let rev_id: Uuid = insert_into(work_rev::table) - .values(work_rev::extra_json.eq(entity.extra)) - .returning(work_rev::id) - .get_result(conn)?; - let ident_id: Uuid = insert_into(work_ident::table) - .values(work_ident::rev_id.eq(rev_id)) - .returning(work_ident::id) - .get_result(conn)?; - let edit: WorkEditRow = insert_into(work_edit::table) - .values((work_edit::editgroup_id.eq(editgroup_id), - work_edit::ident_id.eq(ident_id), - work_edit::rev_id.eq(rev_id))) - .get_result(conn)?; edit.into_model() } @@ -854,8 +426,7 @@ impl Server { editgroup::description.eq(entity.description), editgroup::extra_json.eq(entity.extra), )) - .get_result(conn) - .expect("error creating edit group"); + .get_result(conn)?; Ok(Editgroup { id: Some(uuid2fcid(&row.id)), @@ -1067,31 +638,50 @@ impl Server { Ok(StatsResponse { extra: Some(val) }) } - entity_batch_handler!( - create_container_handler, - create_container_batch_handler, - ContainerEntity - ); - entity_batch_handler!( - create_creator_handler, - create_creator_batch_handler, - CreatorEntity - ); - entity_batch_handler!(create_file_handler, create_file_batch_handler, FileEntity); - entity_batch_handler!( - create_release_handler, - create_release_batch_handler, - ReleaseEntity - ); - entity_batch_handler!(create_work_handler, create_work_batch_handler, WorkEntity); + entity_batch_handler!(create_container_batch_handler, ContainerEntity); + entity_batch_handler!(create_creator_batch_handler, CreatorEntity); + entity_batch_handler!(create_file_batch_handler, FileEntity); + entity_batch_handler!(create_release_batch_handler, ReleaseEntity); + entity_batch_handler!(create_work_batch_handler, WorkEntity); - entity_history_handler!( - get_container_history_handler, - ContainerEditRow, - container_edit - ); - entity_history_handler!(get_creator_history_handler, CreatorEditRow, creator_edit); - entity_history_handler!(get_file_history_handler, FileEditRow, file_edit); - entity_history_handler!(get_release_history_handler, ReleaseEditRow, release_edit); - entity_history_handler!(get_work_history_handler, WorkEditRow, work_edit); + pub fn get_container_history_handler( + &self, + id: &Uuid, + limit: Option<i64>, + conn: &DbConn, + ) -> Result<Vec<EntityHistoryEntry>> { + ContainerEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) + } + pub fn get_creator_history_handler( + &self, + id: &Uuid, + limit: Option<i64>, + conn: &DbConn, + ) -> Result<Vec<EntityHistoryEntry>> { + CreatorEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) + } + pub fn get_file_history_handler( + &self, + id: &Uuid, + limit: Option<i64>, + conn: &DbConn, + ) -> Result<Vec<EntityHistoryEntry>> { + FileEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) + } + pub fn get_release_history_handler( + &self, + id: &Uuid, + limit: Option<i64>, + conn: &DbConn, + ) -> Result<Vec<EntityHistoryEntry>> { + ReleaseEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) + } + pub fn get_work_history_handler( + &self, + id: &Uuid, + limit: Option<i64>, + conn: &DbConn, + ) -> Result<Vec<EntityHistoryEntry>> { + WorkEntity::db_get_history(conn, FatCatId::from_uuid(id), limit) + } } diff --git a/rust/src/api_wrappers.rs b/rust/src/api_wrappers.rs index 95336d3f..faafe984 100644 --- a/rust/src/api_wrappers.rs +++ b/rust/src/api_wrappers.rs @@ -19,8 +19,9 @@ macro_rules! wrap_entity_handlers { // The only stable approach I know of would be: https://github.com/dtolnay/mashup ($get_fn:ident, $get_handler:ident, $get_resp:ident, $post_fn:ident, $post_handler:ident, $post_resp:ident, $post_batch_fn:ident, $post_batch_handler:ident, - $post_batch_resp:ident, $get_history_fn:ident, $get_history_handler:ident, - $get_history_resp:ident, $model:ident) => { + $post_batch_resp:ident, $update_fn:ident, $update_handler:ident, $update_resp:ident, + $delete_fn:ident, $delete_handler:ident, $delete_resp:ident, $get_history_fn:ident, + $get_history_handler:ident, $get_history_resp:ident, $model:ident) => { fn $get_fn( &self, @@ -83,10 +84,12 @@ macro_rules! wrap_entity_handlers { fn $post_batch_fn( &self, entity_list: &Vec<models::$model>, + autoaccept: Option<bool>, + editgroup: Option<String>, _context: &Context, ) -> Box<Future<Item = $post_batch_resp, Error = ApiError> + Send> { let conn = self.db_pool.get().expect("db_pool error"); - let ret = match conn.transaction(|| self.$post_batch_handler(entity_list, &conn)) { + let ret = match conn.transaction(|| self.$post_batch_handler(entity_list, autoaccept.unwrap_or(false), editgroup, &conn)) { Ok(edit) => $post_batch_resp::CreatedEntities(edit), Err(Error(ErrorKind::Diesel(e), _)) => @@ -106,6 +109,79 @@ macro_rules! wrap_entity_handlers { Box::new(futures::done(Ok(ret))) } + fn $update_fn( + &self, + id: String, + entity: models::$model, + _context: &Context, + ) -> Box<Future<Item = $update_resp, Error = ApiError> + Send> { + let id = if let Ok(parsed) = fcid2uuid(&id) { parsed } else { + return Box::new(futures::done(Ok($update_resp::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(id).to_string() })))); + }; + let conn = self.db_pool.get().expect("db_pool error"); + let ret = match conn.transaction(|| self.$update_handler(&id, entity, &conn)) { + Ok(edit) => + $update_resp::UpdatedEntity(edit), + Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => + $update_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), + Err(Error(ErrorKind::Diesel(e), _)) => + $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::Uuid(e), _)) => + $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidFatcatId(e), _)) => + $update_resp::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(e).to_string() }), + Err(Error(ErrorKind::MalformedExternalId(e), _)) => + $update_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(e) => { + error!("{}", e); + $update_resp::GenericError(ErrorResponse { message: e.to_string() }) + }, + }; + Box::new(futures::done(Ok(ret))) + } + + fn $delete_fn( + &self, + id: String, + editgroup_id: Option<String>, + _context: &Context, + ) -> Box<Future<Item = $delete_resp, Error = ApiError> + Send> { + let id = if let Ok(parsed) = fcid2uuid(&id) { parsed } else { + return Box::new(futures::done(Ok($delete_resp::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(id).to_string() })))); + }; + let editgroup_id = match editgroup_id { + Some(raw) => if let Ok(parsed) = fcid2uuid(&raw) { Some(parsed) } else { + return Box::new(futures::done(Ok($delete_resp::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(raw).to_string() })))) + } + None => None + }; + let conn = self.db_pool.get().expect("db_pool error"); + let ret = match conn.transaction(|| self.$delete_handler(&id, editgroup_id, &conn)) { + Ok(edit) => + $delete_resp::DeletedEntity(edit), + Err(Error(ErrorKind::Diesel(::diesel::result::Error::NotFound), _)) => + $delete_resp::NotFound(ErrorResponse { message: format!("No such entity {}: {}", stringify!($model), id) }), + Err(Error(ErrorKind::Diesel(e), _)) => + $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::Uuid(e), _)) => + $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(Error(ErrorKind::InvalidFatcatId(e), _)) => + $delete_resp::BadRequest(ErrorResponse { + message: ErrorKind::InvalidFatcatId(e).to_string() }), + Err(Error(ErrorKind::MalformedExternalId(e), _)) => + $delete_resp::BadRequest(ErrorResponse { message: e.to_string() }), + Err(e) => { + error!("{}", e); + $delete_resp::GenericError(ErrorResponse { message: e.to_string() }) + }, + }; + Box::new(futures::done(Ok(ret))) + } + fn $get_history_fn( &self, id: String, @@ -175,6 +251,12 @@ impl Api for Server { create_container_batch, create_container_batch_handler, CreateContainerBatchResponse, + update_container, + update_container_handler, + UpdateContainerResponse, + delete_container, + delete_container_handler, + DeleteContainerResponse, get_container_history, get_container_history_handler, GetContainerHistoryResponse, @@ -191,6 +273,12 @@ impl Api for Server { create_creator_batch, create_creator_batch_handler, CreateCreatorBatchResponse, + update_creator, + update_creator_handler, + UpdateCreatorResponse, + delete_creator, + delete_creator_handler, + DeleteCreatorResponse, get_creator_history, get_creator_history_handler, GetCreatorHistoryResponse, @@ -206,6 +294,12 @@ impl Api for Server { create_file_batch, create_file_batch_handler, CreateFileBatchResponse, + update_file, + update_file_handler, + UpdateFileResponse, + delete_file, + delete_file_handler, + DeleteFileResponse, get_file_history, get_file_history_handler, GetFileHistoryResponse, @@ -221,6 +315,12 @@ impl Api for Server { create_release_batch, create_release_batch_handler, CreateReleaseBatchResponse, + update_release, + update_release_handler, + UpdateReleaseResponse, + delete_release, + delete_release_handler, + DeleteReleaseResponse, get_release_history, get_release_history_handler, GetReleaseHistoryResponse, @@ -236,6 +336,12 @@ impl Api for Server { create_work_batch, create_work_batch_handler, CreateWorkBatchResponse, + update_work, + update_work_handler, + UpdateWorkResponse, + delete_work, + delete_work_handler, + DeleteWorkResponse, get_work_history, get_work_history_handler, GetWorkHistoryResponse, @@ -308,6 +414,11 @@ impl Api for Server { message: format!("No such editgroup: {}", id), }) } + Err(Error(ErrorKind::EditgroupAlreadyAccepted(e), _)) => { + AcceptEditgroupResponse::BadRequest(ErrorResponse { + message: ErrorKind::EditgroupAlreadyAccepted(e).to_string(), + }) + } Err(e) => AcceptEditgroupResponse::GenericError(ErrorResponse { message: e.to_string(), }), diff --git a/rust/src/database_entity_crud.rs b/rust/src/database_entity_crud.rs new file mode 100644 index 00000000..88c89e84 --- /dev/null +++ b/rust/src/database_entity_crud.rs @@ -0,0 +1,1005 @@ +use api_helpers::*; +use chrono; +use database_models::*; +use database_schema::*; +use diesel::prelude::*; +use diesel::insert_into; +use errors::*; +use fatcat_api::models::*; +use serde_json; +use sha1::Sha1; +use std::marker::Sized; +use std::str::FromStr; +use uuid::Uuid; + +pub struct EditContext { + pub editor_id: FatCatId, + pub editgroup_id: FatCatId, + pub extra_json: Option<serde_json::Value>, + pub autoaccept: bool, +} + +/* One goal here is to abstract the non-entity-specific bits into generic traits or functions, + * instead of macros. + * + * Notably: + * + * db_get + * db_get_rev + * db_create + * db_create_batch + * db_update + * db_delete + * db_get_history + * + * For now, these will probably be macros, until we can level up our trait/generics foo. + */ + +// Associated Type, not parametric +pub trait EntityCrud +where + Self: Sized, +{ + // TODO: could EditRow and IdentRow be generic structs? Or do they need to be bound to a + // specific table? + type EditRow; // EntityEditRow + type EditNewRow; + type IdentRow; // EntityIdentRow + type IdentNewRow; + type RevRow; + + fn parse_editgroup_id(&self) -> Result<Option<FatCatId>>; + + // Generic Methods + fn db_get(conn: &DbConn, ident: FatCatId) -> Result<Self>; + fn db_get_rev(conn: &DbConn, rev_id: Uuid) -> Result<Self>; + fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow>; + fn db_create_batch( + conn: &DbConn, + edit_context: &EditContext, + models: &[&Self], + ) -> Result<Vec<Self::EditRow>>; + fn db_update( + &self, + conn: &DbConn, + edit_context: &EditContext, + ident: FatCatId, + ) -> Result<Self::EditRow>; + fn db_delete( + conn: &DbConn, + edit_context: &EditContext, + ident: FatCatId, + ) -> Result<Self::EditRow>; + fn db_get_history( + conn: &DbConn, + ident: FatCatId, + limit: Option<i64>, + ) -> Result<Vec<EntityHistoryEntry>>; + + // Entity-specific Methods + fn db_from_row( + conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self>; + fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid>; + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>>; +} + +// TODO: this could be a separate trait on all entities +macro_rules! generic_parse_editgroup_id{ + () => { + fn parse_editgroup_id(&self) -> Result<Option<FatCatId>> { + match &self.editgroup_id { + Some(s) => Ok(Some(FatCatId::from_str(&s)?)), + None => Ok(None), + } + } + } +} + +macro_rules! generic_db_get { + ($ident_table:ident, $rev_table:ident) => { + fn db_get(conn: &DbConn, ident: FatCatId) -> Result<Self> { + let (ident, rev): (Self::IdentRow, Self::RevRow) = $ident_table::table + .find(ident.to_uuid()) + .inner_join($rev_table::table) + .first(conn)?; + + Self::db_from_row(conn, rev, Some(ident)) + } + }; +} + +macro_rules! generic_db_get_rev { + ($rev_table:ident) => { + fn db_get_rev(conn: &DbConn, rev_id: Uuid) -> Result<Self> { + let rev = $rev_table::table.find(rev_id).first(conn)?; + + Self::db_from_row(conn, rev, None) + } + }; +} + +macro_rules! generic_db_create { + // TODO: this path should call generic_db_create_batch + ($ident_table: ident, $edit_table: ident) => { + fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow> { + let rev_id = self.db_insert_rev(conn)?; + let ident: Uuid = insert_into($ident_table::table) + .values($ident_table::rev_id.eq(&rev_id)) + .returning($ident_table::id) + .get_result(conn)?; + let edit: Self::EditRow = insert_into($edit_table::table) + .values(( + $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), + $edit_table::rev_id.eq(&rev_id), + $edit_table::ident_id.eq(&ident), + )) + .get_result(conn)?; + Ok(edit) + } + } +} + +macro_rules! generic_db_create_batch { + ($ident_table:ident, $edit_table:ident) => { + fn db_create_batch( + conn: &DbConn, + edit_context: &EditContext, + models: &[&Self], + ) -> Result<Vec<Self::EditRow>> { + let rev_ids: Vec<Uuid> = Self::db_insert_revs(conn, models)?; + let ident_ids: Vec<Uuid> = insert_into($ident_table::table) + .values( + rev_ids + .iter() + .map(|rev_id| Self::IdentNewRow { + rev_id: Some(rev_id.clone()), + is_live: edit_context.autoaccept, + redirect_id: None, + }) + .collect::<Vec<Self::IdentNewRow>>(), + ) + .returning($ident_table::id) + .get_results(conn)?; + let edits: Vec<Self::EditRow> = insert_into($edit_table::table) + .values( + rev_ids + .into_iter() + .zip(ident_ids.into_iter()) + .map(|(rev_id, ident_id)| Self::EditNewRow { + editgroup_id: edit_context.editgroup_id.to_uuid(), + rev_id: Some(rev_id), + ident_id: ident_id, + redirect_id: None, + prev_rev: None, + extra_json: edit_context.extra_json.clone(), + }) + .collect::<Vec<Self::EditNewRow>>(), + ) + .get_results(conn)?; + Ok(edits) + } + }; +} + +macro_rules! generic_db_update { + ($ident_table: ident, $edit_table: ident) => { + fn db_update(&self, conn: &DbConn, edit_context: &EditContext, ident: FatCatId) -> Result<Self::EditRow> { + let current: Self::IdentRow = $ident_table::table.find(ident.to_uuid()).first(conn)?; + if current.is_live != true { + // TODO: what if isn't live? 4xx not 5xx + bail!("can't delete an entity that doesn't exist yet"); + } + if current.rev_id.is_none() { + // TODO: what if it's already deleted? 4xx not 5xx + bail!("entity was already deleted"); + } + + let rev_id = self.db_insert_rev(conn)?; + let edit: Self::EditRow = insert_into($edit_table::table) + .values(( + $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), + $edit_table::ident_id.eq(&ident.to_uuid()), + $edit_table::rev_id.eq(&rev_id), + $edit_table::prev_rev.eq(current.rev_id.unwrap()), + $edit_table::extra_json.eq(&self.extra), + )) + .get_result(conn)?; + + Ok(edit) + } + } +} + +macro_rules! generic_db_delete { + ($ident_table:ident, $edit_table:ident) => { + fn db_delete( + conn: &DbConn, + edit_context: &EditContext, + ident: FatCatId, + ) -> Result<Self::EditRow> { + let current: Self::IdentRow = $ident_table::table.find(ident.to_uuid()).first(conn)?; + if current.is_live != true { + // TODO: what if isn't live? 4xx not 5xx + bail!("can't delete an entity that doesn't exist yet"); + } + if current.rev_id.is_none() { + // TODO: what if it's already deleted? 4xx not 5xx + bail!("entity was already deleted"); + } + let edit: Self::EditRow = insert_into($edit_table::table) + .values(( + $edit_table::editgroup_id.eq(edit_context.editgroup_id.to_uuid()), + $edit_table::ident_id.eq(ident.to_uuid()), + $edit_table::rev_id.eq(None::<Uuid>), + $edit_table::redirect_id.eq(None::<Uuid>), + $edit_table::prev_rev.eq(current.rev_id), + $edit_table::extra_json.eq(&edit_context.extra_json), + )) + .get_result(conn)?; + + Ok(edit) + } + }; +} + +macro_rules! generic_db_get_history { + ($edit_table:ident) => { + fn db_get_history( + conn: &DbConn, + ident: FatCatId, + limit: Option<i64>, + ) -> Result<Vec<EntityHistoryEntry>> { + let limit = limit.unwrap_or(50); // TODO: make a static + + let rows: Vec<(EditgroupRow, ChangelogRow, Self::EditRow)> = editgroup::table + .inner_join(changelog::table) + .inner_join($edit_table::table) + .filter($edit_table::ident_id.eq(ident.to_uuid())) + .order(changelog::id.desc()) + .limit(limit) + .get_results(conn)?; + + let history: Result<Vec<EntityHistoryEntry>> = rows.into_iter() + .map(|(eg_row, cl_row, e_row)| { + Ok(EntityHistoryEntry { + edit: e_row.into_model()?, + editgroup: eg_row.into_model_partial(), + changelog_entry: cl_row.into_model(), + }) + }) + .collect(); + history + } + }; +} + +macro_rules! generic_db_insert_rev { + () => { + fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid> { + Self::db_insert_revs(conn, &vec![self]).map(|id_list| id_list[0]) + } + } +} + +impl EntityCrud for ContainerEntity { + type EditRow = ContainerEditRow; + type EditNewRow = ContainerEditNewRow; + type IdentRow = ContainerIdentRow; + type IdentNewRow = ContainerIdentNewRow; + type RevRow = ContainerRevRow; + + generic_parse_editgroup_id!(); + generic_db_get!(container_ident, container_rev); + generic_db_get_rev!(container_rev); + generic_db_create!(container_ident, container_edit); + generic_db_create_batch!(container_ident, container_edit); + generic_db_update!(container_ident, container_edit); + generic_db_delete!(container_ident, container_edit); + generic_db_get_history!(container_edit); + generic_db_insert_rev!(); + + fn db_from_row( + _conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self> { + let (state, ident_id, redirect_id) = match ident_row { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(FatCatId::from_uuid(&i.id).to_string()), + i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), + ), + None => (None, None, None), + }; + + Ok(ContainerEntity { + issnl: rev_row.issnl, + wikidata_qid: rev_row.wikidata_qid, + publisher: rev_row.publisher, + name: rev_row.name, + abbrev: rev_row.abbrev, + coden: rev_row.coden, + state: state, + ident: ident_id, + revision: Some(rev_row.id.to_string()), + redirect: redirect_id, + extra: rev_row.extra_json, + editgroup_id: None, + }) + } + + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + // first verify external identifier syntax + for entity in models { + if let Some(ref extid) = entity.wikidata_qid { + check_wikidata_qid(extid)?; + } + if let Some(ref extid) = entity.issnl { + check_issn(extid)?; + } + } + + let rev_ids: Vec<Uuid> = insert_into(container_rev::table) + .values( + models + .iter() + .map(|model| ContainerRevNewRow { + name: model.name.clone(), + publisher: model.publisher.clone(), + issnl: model.issnl.clone(), + wikidata_qid: model.wikidata_qid.clone(), + abbrev: model.abbrev.clone(), + coden: model.coden.clone(), + extra_json: model.extra.clone(), + }) + .collect::<Vec<ContainerRevNewRow>>(), + ) + .returning(container_rev::id) + .get_results(conn)?; + Ok(rev_ids) + } +} + +impl EntityCrud for CreatorEntity { + type EditRow = CreatorEditRow; + type EditNewRow = CreatorEditNewRow; + type IdentRow = CreatorIdentRow; + type IdentNewRow = CreatorIdentNewRow; + type RevRow = CreatorRevRow; + + generic_parse_editgroup_id!(); + generic_db_get!(creator_ident, creator_rev); + generic_db_get_rev!(creator_rev); + generic_db_create!(creator_ident, creator_edit); + generic_db_create_batch!(creator_ident, creator_edit); + generic_db_update!(creator_ident, creator_edit); + generic_db_delete!(creator_ident, creator_edit); + generic_db_get_history!(creator_edit); + generic_db_insert_rev!(); + + fn db_from_row( + _conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self> { + let (state, ident_id, redirect_id) = match ident_row { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(FatCatId::from_uuid(&i.id).to_string()), + i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), + ), + None => (None, None, None), + }; + Ok(CreatorEntity { + display_name: rev_row.display_name, + given_name: rev_row.given_name, + surname: rev_row.surname, + orcid: rev_row.orcid, + wikidata_qid: rev_row.wikidata_qid, + state: state, + ident: ident_id, + revision: Some(rev_row.id.to_string()), + redirect: redirect_id, + editgroup_id: None, + extra: rev_row.extra_json, + }) + } + + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + // first verify external identifier syntax + for entity in models { + if let Some(ref extid) = entity.orcid { + check_orcid(extid)?; + } + if let Some(ref extid) = entity.wikidata_qid { + check_wikidata_qid(extid)?; + } + } + + let rev_ids: Vec<Uuid> = insert_into(creator_rev::table) + .values( + models + .iter() + .map(|model| CreatorRevNewRow { + display_name: model.display_name.clone(), + given_name: model.given_name.clone(), + surname: model.surname.clone(), + orcid: model.orcid.clone(), + wikidata_qid: model.wikidata_qid.clone(), + extra_json: model.extra.clone(), + }) + .collect::<Vec<CreatorRevNewRow>>(), + ) + .returning(creator_rev::id) + .get_results(conn)?; + Ok(rev_ids) + } +} + +impl EntityCrud for FileEntity { + type EditRow = FileEditRow; + type EditNewRow = FileEditNewRow; + type IdentRow = FileIdentRow; + type IdentNewRow = FileIdentNewRow; + type RevRow = FileRevRow; + + generic_parse_editgroup_id!(); + generic_db_get!(file_ident, file_rev); + generic_db_get_rev!(file_rev); + generic_db_create!(file_ident, file_edit); + generic_db_create_batch!(file_ident, file_edit); + generic_db_update!(file_ident, file_edit); + generic_db_delete!(file_ident, file_edit); + generic_db_get_history!(file_edit); + generic_db_insert_rev!(); + + fn db_from_row( + conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self> { + let (state, ident_id, redirect_id) = match ident_row { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(FatCatId::from_uuid(&i.id).to_string()), + i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), + ), + None => (None, None, None), + }; + + let releases: Vec<FatCatId> = file_release::table + .filter(file_release::file_rev.eq(rev_row.id)) + .get_results(conn)? + .into_iter() + .map(|r: FileReleaseRow| FatCatId::from_uuid(&r.target_release_ident_id)) + .collect(); + + let urls: Vec<FileEntityUrls> = file_rev_url::table + .filter(file_rev_url::file_rev.eq(rev_row.id)) + .get_results(conn)? + .into_iter() + .map(|r: FileRevUrlRow| FileEntityUrls { + rel: r.rel, + url: r.url, + }) + .collect(); + + Ok(FileEntity { + sha1: rev_row.sha1, + sha256: rev_row.sha256, + md5: rev_row.md5, + size: rev_row.size.map(|v| v as i64), + urls: Some(urls), + mimetype: rev_row.mimetype, + releases: Some(releases.iter().map(|fcid| fcid.to_string()).collect()), + state: state, + ident: ident_id, + revision: Some(rev_row.id.to_string()), + redirect: redirect_id, + editgroup_id: None, + extra: rev_row.extra_json, + }) + } + + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + let rev_ids: Vec<Uuid> = insert_into(file_rev::table) + .values( + models + .iter() + .map(|model| FileRevNewRow { + size: model.size, + sha1: model.sha1.clone(), + sha256: model.sha256.clone(), + md5: model.md5.clone(), + mimetype: model.mimetype.clone(), + extra_json: model.extra.clone(), + }) + .collect::<Vec<FileRevNewRow>>(), + ) + .returning(file_rev::id) + .get_results(conn)?; + + let mut file_release_rows: Vec<FileReleaseRow> = vec![]; + let mut file_url_rows: Vec<FileRevUrlNewRow> = vec![]; + + for (model, rev_id) in models.iter().zip(rev_ids.iter()) { + match &model.releases { + None => (), + Some(release_list) => { + let these_release_rows: Result<Vec<FileReleaseRow>> = release_list + .iter() + .map(|r| { + Ok(FileReleaseRow { + file_rev: rev_id.clone(), + target_release_ident_id: FatCatId::from_str(r)?.to_uuid(), + }) + }) + .collect(); + file_release_rows.extend(these_release_rows?); + } + }; + + match &model.urls { + None => (), + Some(url_list) => { + let these_url_rows: Vec<FileRevUrlNewRow> = url_list + .into_iter() + .map(|u| FileRevUrlNewRow { + file_rev: rev_id.clone(), + rel: u.rel.clone(), + url: u.url.clone(), + }) + .collect(); + file_url_rows.extend(these_url_rows); + } + }; + } + + if !file_release_rows.is_empty() { + // TODO: shouldn't it be "file_rev_release"? + insert_into(file_release::table) + .values(file_release_rows) + .execute(conn)?; + } + + if !file_url_rows.is_empty() { + insert_into(file_rev_url::table) + .values(file_url_rows) + .execute(conn)?; + } + + Ok(rev_ids) + } +} + +impl EntityCrud for ReleaseEntity { + type EditRow = ReleaseEditRow; + type EditNewRow = ReleaseEditNewRow; + type IdentRow = ReleaseIdentRow; + type IdentNewRow = ReleaseIdentNewRow; + type RevRow = ReleaseRevRow; + + generic_parse_editgroup_id!(); + generic_db_get!(release_ident, release_rev); + generic_db_get_rev!(release_rev); + //generic_db_create!(release_ident, release_edit); + //generic_db_create_batch!(release_ident, release_edit); + generic_db_update!(release_ident, release_edit); + generic_db_delete!(release_ident, release_edit); + generic_db_get_history!(release_edit); + generic_db_insert_rev!(); + + fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow> { + let mut edits = Self::db_create_batch(conn, edit_context, &vec![self])?; + // probably a more elegant way to destroy the vec and take first element + Ok(edits.pop().unwrap()) + } + + fn db_create_batch( + conn: &DbConn, + edit_context: &EditContext, + models: &[&Self], + ) -> Result<Vec<Self::EditRow>> { + // This isn't the generic implementation because we need to create Work entities for each + // of the release entities passed (at least in the common case) + + // Generate the set of new work entities to insert (usually one for each release, but some + // releases might be pointed to a work already) + let mut new_work_models: Vec<&WorkEntity> = vec![]; + for entity in models { + if entity.work_id.is_none() { + new_work_models.push(&WorkEntity { + ident: None, + revision: None, + redirect: None, + state: None, + editgroup_id: None, + extra: None, + }); + }; + } + + // create the works, then pluck the list of idents from the result + let new_work_edits = + WorkEntity::db_create_batch(conn, edit_context, new_work_models.as_slice())?; + let mut new_work_ids: Vec<Uuid> = new_work_edits.iter().map(|edit| edit.ident_id).collect(); + + // Copy all the release models, and ensure that each has work_id set, using the new work + // idents. There should be one new work ident for each release missing one. + let models_with_work_ids: Vec<Self> = models + .iter() + .map(|model| { + let mut model = (*model).clone(); + if model.work_id.is_none() { + model.work_id = + Some(FatCatId::from_uuid(&new_work_ids.pop().unwrap()).to_string()) + } + model + }) + .collect(); + let model_refs: Vec<&Self> = models_with_work_ids.iter().map(|s| s).collect(); + let models = model_refs.as_slice(); + + // The rest here is copy/pasta from the generic (how to avoid copypasta?) + let rev_ids: Vec<Uuid> = Self::db_insert_revs(conn, models)?; + let ident_ids: Vec<Uuid> = insert_into(release_ident::table) + .values( + rev_ids + .iter() + .map(|rev_id| Self::IdentNewRow { + rev_id: Some(rev_id.clone()), + is_live: edit_context.autoaccept, + redirect_id: None, + }) + .collect::<Vec<Self::IdentNewRow>>(), + ) + .returning(release_ident::id) + .get_results(conn)?; + let edits: Vec<Self::EditRow> = insert_into(release_edit::table) + .values( + rev_ids + .into_iter() + .zip(ident_ids.into_iter()) + .map(|(rev_id, ident_id)| Self::EditNewRow { + editgroup_id: edit_context.editgroup_id.to_uuid(), + rev_id: Some(rev_id), + ident_id: ident_id, + redirect_id: None, + prev_rev: None, + extra_json: edit_context.extra_json.clone(), + }) + .collect::<Vec<Self::EditNewRow>>(), + ) + .get_results(conn)?; + Ok(edits) + } + + fn db_from_row( + conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self> { + let (state, ident_id, redirect_id) = match ident_row { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(FatCatId::from_uuid(&i.id).to_string()), + i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), + ), + None => (None, None, None), + }; + + let refs: Vec<ReleaseRef> = release_ref::table + .filter(release_ref::release_rev.eq(rev_row.id)) + .order(release_ref::index_val.asc()) + .get_results(conn)? + .into_iter() + .map(|r: ReleaseRefRow| ReleaseRef { + index: r.index_val, + key: r.key, + extra: r.extra_json, + container_title: r.container_title, + year: r.year, + title: r.title, + locator: r.locator, + target_release_id: r.target_release_ident_id + .map(|v| FatCatId::from_uuid(&v).to_string()), + }) + .collect(); + + let contribs: Vec<ReleaseContrib> = release_contrib::table + .filter(release_contrib::release_rev.eq(rev_row.id)) + .order(( + release_contrib::role.asc(), + release_contrib::index_val.asc(), + )) + .get_results(conn)? + .into_iter() + .map(|c: ReleaseContribRow| ReleaseContrib { + index: c.index_val, + raw_name: c.raw_name, + role: c.role, + extra: c.extra_json, + creator_id: c.creator_ident_id + .map(|v| FatCatId::from_uuid(&v).to_string()), + creator: None, + }) + .collect(); + + let abstracts: Vec<ReleaseEntityAbstracts> = release_rev_abstract::table + .inner_join(abstracts::table) + .filter(release_rev_abstract::release_rev.eq(rev_row.id)) + .get_results(conn)? + .into_iter() + .map( + |r: (ReleaseRevAbstractRow, AbstractsRow)| ReleaseEntityAbstracts { + sha1: Some(r.0.abstract_sha1), + mimetype: r.0.mimetype, + lang: r.0.lang, + content: Some(r.1.content), + }, + ) + .collect(); + + Ok(ReleaseEntity { + title: rev_row.title, + release_type: rev_row.release_type, + release_status: rev_row.release_status, + release_date: rev_row + .release_date + .map(|v| chrono::DateTime::from_utc(v.and_hms(0, 0, 0), chrono::Utc)), + doi: rev_row.doi, + pmid: rev_row.pmid, + pmcid: rev_row.pmcid, + isbn13: rev_row.isbn13, + core_id: rev_row.core_id, + wikidata_qid: rev_row.wikidata_qid, + volume: rev_row.volume, + issue: rev_row.issue, + pages: rev_row.pages, + files: None, + container: None, + container_id: rev_row + .container_ident_id + .map(|u| FatCatId::from_uuid(&u).to_string()), + publisher: rev_row.publisher, + language: rev_row.language, + work_id: Some(FatCatId::from_uuid(&rev_row.work_ident_id).to_string()), + refs: Some(refs), + contribs: Some(contribs), + abstracts: Some(abstracts), + state: state, + ident: ident_id, + revision: Some(rev_row.id.to_string()), + redirect: redirect_id, + editgroup_id: None, + extra: rev_row.extra_json, + }) + } + + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + // first verify external identifier syntax + for entity in models { + if let Some(ref extid) = entity.doi { + check_doi(extid)?; + } + if let Some(ref extid) = entity.pmid { + check_pmid(extid)?; + } + if let Some(ref extid) = entity.pmcid { + check_pmcid(extid)?; + } + if let Some(ref extid) = entity.wikidata_qid { + check_wikidata_qid(extid)?; + } + } + + let rev_ids: Vec<Uuid> = insert_into(release_rev::table) + .values(models + .iter() + .map(|model| { + Ok(ReleaseRevNewRow { + title: model.title.clone(), + release_type: model.release_type.clone(), + release_status: model.release_status.clone(), + release_date: model.release_date.map(|v| v.naive_utc().date()), + doi: model.doi.clone(), + pmid: model.pmid.clone(), + pmcid: model.pmcid.clone(), + wikidata_qid: model.wikidata_qid.clone(), + isbn13: model.isbn13.clone(), + core_id: model.core_id.clone(), + volume: model.volume.clone(), + issue: model.issue.clone(), + pages: model.pages.clone(), + work_ident_id: match model.work_id.clone() { + None => bail!("release_revs must have a work_id by the time they are inserted; this is an internal soundness error"), + Some(s) => FatCatId::from_str(&s)?.to_uuid(), + }, + container_ident_id: match model.container_id.clone() { + None => None, + Some(s) => Some(FatCatId::from_str(&s)?.to_uuid()), + }, + publisher: model.publisher.clone(), + language: model.language.clone(), + extra_json: model.extra.clone() + }) + }) + .collect::<Result<Vec<ReleaseRevNewRow>>>()?) + .returning(release_rev::id) + .get_results(conn)?; + + let mut release_ref_rows: Vec<ReleaseRefNewRow> = vec![]; + let mut release_contrib_rows: Vec<ReleaseContribNewRow> = vec![]; + let mut abstract_rows: Vec<AbstractsRow> = vec![]; + let mut release_abstract_rows: Vec<ReleaseRevAbstractNewRow> = vec![]; + + for (model, rev_id) in models.iter().zip(rev_ids.iter()) { + match &model.refs { + None => (), + Some(ref_list) => { + let these_ref_rows: Vec<ReleaseRefNewRow> = ref_list + .iter() + .map(|r| { + Ok(ReleaseRefNewRow { + release_rev: rev_id.clone(), + target_release_ident_id: match r.target_release_id.clone() { + None => None, + Some(v) => Some(FatCatId::from_str(&v)?.to_uuid()), + }, + index_val: r.index, + key: r.key.clone(), + container_title: r.container_title.clone(), + year: r.year, + title: r.title.clone(), + locator: r.locator.clone(), + extra_json: r.extra.clone(), + }) + }) + .collect::<Result<Vec<ReleaseRefNewRow>>>()?; + release_ref_rows.extend(these_ref_rows); + } + }; + + match &model.contribs { + None => (), + Some(contrib_list) => { + let these_contrib_rows: Vec<ReleaseContribNewRow> = contrib_list + .iter() + .map(|c| { + Ok(ReleaseContribNewRow { + release_rev: rev_id.clone(), + creator_ident_id: match c.creator_id.clone() { + None => None, + Some(v) => Some(FatCatId::from_str(&v)?.to_uuid()), + }, + raw_name: c.raw_name.clone(), + index_val: c.index, + role: c.role.clone(), + extra_json: c.extra.clone(), + }) + }) + .collect::<Result<Vec<ReleaseContribNewRow>>>()?; + release_contrib_rows.extend(these_contrib_rows); + } + }; + + if let Some(abstract_list) = &model.abstracts { + // For rows that specify content, we need to insert the abstract if it doesn't exist + // already + let new_abstracts: Vec<AbstractsRow> = abstract_list + .iter() + .filter(|ea| ea.content.is_some()) + .map(|c| AbstractsRow { + sha1: Sha1::from(c.content.clone().unwrap()).hexdigest(), + content: c.content.clone().unwrap(), + }) + .collect(); + abstract_rows.extend(new_abstracts); + let new_release_abstract_rows: Vec<ReleaseRevAbstractNewRow> = abstract_list + .into_iter() + .map(|c| { + Ok(ReleaseRevAbstractNewRow { + release_rev: rev_id.clone(), + abstract_sha1: match c.content { + Some(ref content) => Sha1::from(content).hexdigest(), + None => match c.sha1.clone() { + Some(v) => v, + None => bail!("either abstract_sha1 or content is required"), + }, + }, + lang: c.lang.clone(), + mimetype: c.mimetype.clone(), + }) + }) + .collect::<Result<Vec<ReleaseRevAbstractNewRow>>>()?; + release_abstract_rows.extend(new_release_abstract_rows); + } + } + + if !release_ref_rows.is_empty() { + insert_into(release_ref::table) + .values(release_ref_rows) + .execute(conn)?; + } + + if !release_contrib_rows.is_empty() { + insert_into(release_contrib::table) + .values(release_contrib_rows) + .execute(conn)?; + } + + if !abstract_rows.is_empty() { + // Sort of an "upsert"; only inserts new abstract rows if they don't already exist + insert_into(abstracts::table) + .values(&abstract_rows) + .on_conflict(abstracts::sha1) + .do_nothing() + .execute(conn)?; + insert_into(release_rev_abstract::table) + .values(release_abstract_rows) + .execute(conn)?; + } + + Ok(rev_ids) + } +} + +impl EntityCrud for WorkEntity { + type EditRow = WorkEditRow; + type EditNewRow = WorkEditNewRow; + type IdentRow = WorkIdentRow; + type IdentNewRow = WorkIdentNewRow; + type RevRow = WorkRevRow; + + generic_parse_editgroup_id!(); + generic_db_get!(work_ident, work_rev); + generic_db_get_rev!(work_rev); + generic_db_create!(work_ident, work_edit); + generic_db_create_batch!(work_ident, work_edit); + generic_db_update!(work_ident, work_edit); + generic_db_delete!(work_ident, work_edit); + generic_db_get_history!(work_edit); + generic_db_insert_rev!(); + + fn db_from_row( + _conn: &DbConn, + rev_row: Self::RevRow, + ident_row: Option<Self::IdentRow>, + ) -> Result<Self> { + let (state, ident_id, redirect_id) = match ident_row { + Some(i) => ( + Some(i.state().unwrap().shortname()), + Some(FatCatId::from_uuid(&i.id).to_string()), + i.redirect_id.map(|u| FatCatId::from_uuid(&u).to_string()), + ), + None => (None, None, None), + }; + + Ok(WorkEntity { + state: state, + ident: ident_id, + revision: Some(rev_row.id.to_string()), + redirect: redirect_id, + editgroup_id: None, + extra: rev_row.extra_json, + }) + } + + fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> { + let rev_ids: Vec<Uuid> = insert_into(work_rev::table) + .values( + models + .iter() + .map(|model| WorkRevNewRow { + extra_json: model.extra.clone(), + }) + .collect::<Vec<WorkRevNewRow>>(), + ) + .returning(work_rev::id) + .get_results(conn)?; + Ok(rev_ids) + } +} diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 47e00bcf..93e6a0fe 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -37,7 +37,14 @@ pub trait EntityEditRow { // Helper for constructing tables macro_rules! entity_structs { - ($edit_table:expr, $edit_struct:ident, $ident_table:expr, $ident_struct:ident) => { + ( + $edit_table:expr, + $edit_struct:ident, + $edit_new_struct:ident, + $ident_table:expr, + $ident_struct:ident, + $ident_new_struct:ident + ) => { #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset, QueryableByName)] #[table_name = $edit_table] pub struct $edit_struct { @@ -51,6 +58,17 @@ macro_rules! entity_structs { pub extra_json: Option<serde_json::Value>, } + #[derive(Debug, Associations, AsChangeset, QueryableByName, Insertable)] + #[table_name = $edit_table] + pub struct $edit_new_struct { + pub editgroup_id: Uuid, + pub ident_id: Uuid, + pub rev_id: Option<Uuid>, + pub redirect_id: Option<Uuid>, + pub prev_rev: Option<Uuid>, + pub extra_json: Option<serde_json::Value>, + } + impl EntityEditRow for $edit_struct { /// Go from a row (SQL model) to an API model fn into_model(self) -> Result<EntityEdit> { @@ -75,6 +93,14 @@ macro_rules! entity_structs { pub redirect_id: Option<Uuid>, } + #[derive(Debug, Associations, AsChangeset, Insertable)] + #[table_name = $ident_table] + pub struct $ident_new_struct { + pub is_live: bool, + pub rev_id: Option<Uuid>, + pub redirect_id: Option<Uuid>, + } + impl EntityIdentRow for $ident_struct { fn state(&self) -> Result<EntityState> { if !self.is_live { @@ -104,11 +130,25 @@ pub struct ContainerRevRow { pub coden: Option<String>, } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "container_rev"] +pub struct ContainerRevNewRow { + pub extra_json: Option<serde_json::Value>, + pub name: String, + pub publisher: Option<String>, + pub issnl: Option<String>, + pub wikidata_qid: Option<String>, + pub abbrev: Option<String>, + pub coden: Option<String>, +} + entity_structs!( "container_edit", ContainerEditRow, + ContainerEditNewRow, "container_ident", - ContainerIdentRow + ContainerIdentRow, + ContainerIdentNewRow ); #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -123,11 +163,24 @@ pub struct CreatorRevRow { pub wikidata_qid: Option<String>, } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "creator_rev"] +pub struct CreatorRevNewRow { + pub extra_json: Option<serde_json::Value>, + pub display_name: String, + pub given_name: Option<String>, + pub surname: Option<String>, + pub orcid: Option<String>, + pub wikidata_qid: Option<String>, +} + entity_structs!( "creator_edit", CreatorEditRow, + CreatorEditNewRow, "creator_ident", - CreatorIdentRow + CreatorIdentRow, + CreatorIdentNewRow ); #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -159,7 +212,25 @@ pub struct FileRevRow { pub mimetype: Option<String>, } -entity_structs!("file_edit", FileEditRow, "file_ident", FileIdentRow); +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "file_rev"] +pub struct FileRevNewRow { + pub extra_json: Option<serde_json::Value>, + pub size: Option<i64>, + pub sha1: Option<String>, + pub sha256: Option<String>, + pub md5: Option<String>, + pub mimetype: Option<String>, +} + +entity_structs!( + "file_edit", + FileEditRow, + FileEditNewRow, + "file_ident", + FileIdentRow, + FileIdentNewRow +); #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] #[table_name = "release_rev"] @@ -185,11 +256,36 @@ pub struct ReleaseRevRow { pub language: Option<String>, } +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "release_rev"] +pub struct ReleaseRevNewRow { + pub extra_json: Option<serde_json::Value>, + pub work_ident_id: Uuid, + pub container_ident_id: Option<Uuid>, + pub title: String, + pub release_type: Option<String>, + pub release_status: Option<String>, + pub release_date: Option<chrono::NaiveDate>, + pub doi: Option<String>, + pub pmid: Option<String>, + pub pmcid: Option<String>, + pub wikidata_qid: Option<String>, + pub isbn13: Option<String>, + pub core_id: Option<String>, + pub volume: Option<String>, + pub issue: Option<String>, + pub pages: Option<String>, + pub publisher: Option<String>, + pub language: Option<String>, +} + entity_structs!( "release_edit", ReleaseEditRow, + ReleaseEditNewRow, "release_ident", - ReleaseIdentRow + ReleaseIdentRow, + ReleaseIdentNewRow ); #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] @@ -199,7 +295,20 @@ pub struct WorkRevRow { pub extra_json: Option<serde_json::Value>, } -entity_structs!("work_edit", WorkEditRow, "work_ident", WorkIdentRow); +#[derive(Debug, Associations, AsChangeset, Insertable)] +#[table_name = "work_rev"] +pub struct WorkRevNewRow { + pub extra_json: Option<serde_json::Value>, +} + +entity_structs!( + "work_edit", + WorkEditRow, + WorkEditNewRow, + "work_ident", + WorkIdentRow, + WorkIdentNewRow +); #[derive(Debug, Queryable, Identifiable, Associations, AsChangeset)] #[table_name = "release_rev_abstract"] diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 356084a5..2236d602 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -25,6 +25,7 @@ extern crate sha1; pub mod api_helpers; pub mod api_server; pub mod api_wrappers; +pub mod database_entity_crud; pub mod database_models; pub mod database_schema; @@ -47,6 +48,10 @@ mod errors { description("external identifier doesn't match required pattern") display("external identifier doesn't match required pattern") } + EditgroupAlreadyAccepted(id: String) { + description("editgroup was already accepted") + display("attempted to accept an editgroup which was already accepted: {}", id) + } } } } |