aboutsummaryrefslogtreecommitdiffstats
path: root/rust/src
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src')
-rw-r--r--rust/src/api_helpers.rs44
-rw-r--r--rust/src/api_server.rs858
-rw-r--r--rust/src/api_wrappers.rs117
-rw-r--r--rust/src/database_entity_crud.rs1005
-rw-r--r--rust/src/database_models.rs121
-rw-r--r--rust/src/lib.rs5
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(&param)?,
- };
- 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(&param)?,
- };
- 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(&param)?,
- };
-
- 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(&param)?,
- };
- 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(&param)?,
- };
+ 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)
+ }
}
}
}