diff options
Diffstat (limited to 'rust')
| -rw-r--r-- | rust/src/api_server.rs | 687 | ||||
| -rw-r--r-- | rust/src/database_entity_crud.rs | 626 | ||||
| -rw-r--r-- | rust/src/database_models.rs | 57 | 
3 files changed, 745 insertions, 625 deletions
| diff --git a/rust/src/api_server.rs b/rust/src/api_server.rs index 5060ba0f..ecf0c242 100644 --- a/rust/src/api_server.rs +++ b/rust/src/api_server.rs @@ -21,47 +21,16 @@ use database_entity_crud::{EntityCrud, EditContext};  use std::str::FromStr;  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],              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 edit_context = make_edit_context(conn, None)?; +            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())?; +            edits.into_iter().map(|e| e.into_model()).collect()          }      }  } @@ -96,226 +65,6 @@ 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), -    }; -    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, -    }) -} - -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, -    }) -} - -/* XXX: -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, -    }) -} -*/ -  impl Server {      pub fn get_container_handler(          &self, @@ -323,13 +72,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> { @@ -344,7 +87,7 @@ impl Server {              .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( @@ -353,12 +96,8 @@ 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> { @@ -373,7 +112,7 @@ impl Server {              .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( @@ -392,8 +131,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()      } @@ -403,12 +143,7 @@ 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> { @@ -422,7 +157,7 @@ impl Server {              .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( @@ -431,12 +166,8 @@ 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() { @@ -447,7 +178,6 @@ impl Server {                      Some(self.get_container_handler(&fcid2uuid(&cid)?, None, conn)?);              }          } -          Ok(release)      } @@ -463,22 +193,23 @@ impl Server {              .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()      } @@ -502,7 +233,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()      } @@ -511,52 +242,25 @@ 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: ContainerEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO container_rev (name, publisher, issnl, wikidata_qid, abbrev, coden, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6, $7) -                        RETURNING id ), -                ident AS ( INSERT INTO container_ident (rev_id) -                            VALUES ((SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO container_edit (editgroup_id, ident_id, rev_id) VALUES -                ($8, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.publisher) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.issnl) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.abbrev) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.coden) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -            .get_result(conn)?; - +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?;          edit.into_model()      } -    // XXX:      pub fn update_container_handler(          &self,          id: &Uuid,          entity: models::ContainerEntity,          conn: &DbConn,      ) -> Result<EntityEdit> { -        unimplemented!() +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        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> { -        unimplemented!() +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = ContainerEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model()      }      pub fn create_creator_handler( @@ -564,51 +268,26 @@ 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 edit: CreatorEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO creator_rev (display_name, given_name, surname, orcid, wikidata_qid, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6) -                        RETURNING id ), -                ident AS ( INSERT INTO creator_ident (rev_id) -                            VALUES ((SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO creator_edit (editgroup_id, ident_id, rev_id) VALUES -                ($7, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.display_name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.given_name) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.surname) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.orcid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_id) -            .get_result(conn)?; - +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        let edit = entity.db_create(conn, &edit_context)?;          edit.into_model() +      } -    // XXX:      pub fn update_creator_handler(          &self,          id: &Uuid,          entity: models::CreatorEntity,          conn: &DbConn,      ) -> Result<EntityEdit> { -        unimplemented!() +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        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> { -        unimplemented!() +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = CreatorEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model()      }      pub fn create_file_handler( @@ -616,92 +295,25 @@ 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 edit: FileEditRow = -            diesel::sql_query( -                "WITH rev AS ( INSERT INTO file_rev (size, sha1, sha256, md5, mimetype, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6) -                        RETURNING id ), -                ident AS ( INSERT INTO file_ident (rev_id) -                            VALUES ((SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO file_edit (editgroup_id, ident_id, rev_id) VALUES -                ($7, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -            ).bind::<diesel::sql_types::Nullable<diesel::sql_types::Int8>, _>(entity.size) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.sha1) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.sha256) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.md5) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.mimetype) -                .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -                .bind::<diesel::sql_types::Uuid, _>(editgroup_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()?)?; +        let edit = entity.db_create(conn, &edit_context)?;          edit.into_model()      } -    // XXX:      pub fn update_file_handler(          &self,          id: &Uuid,          entity: models::FileEntity,          conn: &DbConn,      ) -> Result<EntityEdit> { -        unimplemented!() +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        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> { -        unimplemented!() +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = FileEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model()      }      pub fn create_release_handler( @@ -709,187 +321,25 @@ 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 edit: ReleaseEditRow = diesel::sql_query( -            "WITH rev AS ( INSERT INTO release_rev (title, release_type, release_status, release_date, doi, pmid, pmcid, wikidata_qid, isbn13, core_id, volume, issue, pages, work_ident_id, container_ident_id, publisher, language, extra_json) -                        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) -                        RETURNING id ), -                ident AS ( INSERT INTO release_ident (rev_id) -                            VALUES ((SELECT rev.id FROM rev)) -                            RETURNING id ) -            INSERT INTO release_edit (editgroup_id, ident_id, rev_id) VALUES -                ($19, (SELECT ident.id FROM ident), (SELECT rev.id FROM rev)) -            RETURNING *", -        ).bind::<diesel::sql_types::Text, _>(entity.title) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.release_type) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.release_status) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Date>, _>( -                entity.release_date.map(|v| v.naive_utc().date())) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.doi) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pmid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pmcid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.wikidata_qid) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.isbn13) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.core_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.volume) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.issue) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.pages) -            .bind::<diesel::sql_types::Uuid, _>(work_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Uuid>, _>(container_id) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.publisher) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Text>, _>(entity.language) -            .bind::<diesel::sql_types::Nullable<diesel::sql_types::Json>, _>(entity.extra) -            .bind::<diesel::sql_types::Uuid, _>(editgroup_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()?)?; +        let edit = entity.db_create(conn, &edit_context)?;          edit.into_model()      } -    // XXX:      pub fn update_release_handler(          &self,          id: &Uuid,          entity: models::ReleaseEntity,          conn: &DbConn,      ) -> Result<EntityEdit> { -        unimplemented!() +        let edit_context = make_edit_context(conn, entity.parse_editgroup_id()?)?; +        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> { -        unimplemented!() +        let edit_context = make_edit_context(conn, editgroup_id.map(|u| FatCatId::from_uuid(&u)))?; +        let edit = ReleaseEntity::db_delete(conn, &edit_context, FatCatId::from_uuid(id))?; +        edit.into_model()      }      pub fn create_work_handler( @@ -1150,30 +600,33 @@ impl Server {      }      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_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_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/database_entity_crud.rs b/rust/src/database_entity_crud.rs index e97e134b..b8ff195a 100644 --- a/rust/src/database_entity_crud.rs +++ b/rust/src/database_entity_crud.rs @@ -1,11 +1,13 @@ +use sha1::Sha1; +use chrono;  use diesel::prelude::*;  use diesel::{self, insert_into};  use database_schema::*;  use database_models::*;  use errors::*;  use fatcat_api::models::*; -use api_helpers::{FatCatId, DbConn}; +use api_helpers::*;  use uuid::Uuid;  use std::marker::Sized;  use std::str::FromStr; @@ -55,7 +57,7 @@ pub trait EntityCrud where Self: Sized {      fn db_get_history(conn: &DbConn, ident: FatCatId, limit: Option<i64>) -> Result<Vec<EntityHistoryEntry>>;      // Entity-specific Methods -    fn from_row(conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self>; +    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>>;  } @@ -80,7 +82,7 @@ macro_rules! generic_db_get {                  .inner_join($rev_table::table)                  .first(conn)?; -            Self::from_row(conn, rev, Some(ident)) +            Self::db_from_row(conn, rev, Some(ident))          }      }  } @@ -92,12 +94,13 @@ macro_rules! generic_db_get_rev {                  .find(rev_id)                  .first(conn)?; -            Self::from_row(conn, rev, None) +            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)?; @@ -128,7 +131,7 @@ macro_rules! generic_db_create_batch {                          is_live: edit_context.autoapprove,                          redirect_id: None,                      }) -                    .collect::<Vec<WorkIdentNewRow>>()) +                    .collect::<Vec<Self::IdentNewRow>>())                  .returning($ident_table::id)                  .get_results(conn)?;              let edits: Vec<Self::EditRow> = insert_into($edit_table::table) @@ -141,7 +144,7 @@ macro_rules! generic_db_create_batch {                          prev_rev: None,                          extra_json: edit_context.extra_json.clone(),                      }) -                    .collect::<Vec<WorkEditNewRow>>()) +                    .collect::<Vec<Self::EditNewRow>>())                  .get_results(conn)?;              Ok(edits)          } @@ -209,7 +212,7 @@ macro_rules! generic_db_delete {  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); // XXX: make a static +            let limit = limit.unwrap_or(50); // TODO: make a static              let rows: Vec<(EditgroupRow, ChangelogRow, Self::EditRow)> = editgroup::table                  .inner_join(changelog::table) @@ -239,6 +242,613 @@ macro_rules! generic_db_insert_rev {      }  } +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: Vec<FileReleaseRow> = release_list +                        .iter() +                        .map(|r| FileReleaseRow { +                            file_rev: rev_id.clone(), +                            target_release_ident_id: fcid2uuid(r) +                                // XXX: shouldn't expect +                                .expect("invalid fatcat identifier"), +                        }) +                        .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.autoapprove, +                    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) +            .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| 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) +            .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| 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| 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: model.work_id +                        .clone() +                        .map(|s| FatCatId::from_str(&s).expect("invalid fatcat identifier").to_uuid()) +                        .expect("release_revs must have a work_id by the time they are inserted; this is an internal soundness error"), +                    container_ident_id: model.container_id +                        .clone() +                        .map(|s| FatCatId::from_str(&s).expect("invalid fatcat identifier").to_uuid()), +                    publisher: model.publisher.clone(), +                    language: model.language.clone(), +                    extra_json: model.extra.clone() +                }) +                .collect::<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![]; + +        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| ReleaseRefNewRow { +                            release_rev: rev_id.clone(), +                            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(); +                    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| ReleaseContribNewRow { +                            release_rev: rev_id.clone(), +                            creator_ident_id: c.creator_id +                                .clone() +                                // XXX: shouldn't have these expects +                                .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(); +                    release_contrib_rows.extend(these_contrib_rows); +                } +            }; + +            // TODO: this part still isn't parallelized +            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(); +                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: rev_id.clone(), +                        abstract_sha1: match c.content { +                            Some(ref content) => Sha1::from(content).hexdigest(), +                            // XXX: shouldn't have these expects +                            None => c.sha1.clone().expect("either abstract_sha1 or content is required"), +                        }, +                        lang: c.lang.clone(), +                        mimetype: c.mimetype.clone(), +                    }) +                    .collect(); +                insert_into(release_rev_abstract::table) +                    .values(release_abstract_rows) +                    .execute(conn)?; +            } +        } + +        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)?; +        } + +        Ok(rev_ids) +    } +} +  impl EntityCrud for WorkEntity {      type EditRow = WorkEditRow;      type EditNewRow = WorkEditNewRow; @@ -256,7 +866,7 @@ impl EntityCrud for WorkEntity {      generic_db_get_history!(work_edit);      generic_db_insert_rev!(); -    fn from_row(conn: &DbConn, rev_row: Self::RevRow, ident_row: Option<Self::IdentRow>) -> Result<Self> { +    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) => ( diff --git a/rust/src/database_models.rs b/rust/src/database_models.rs index 14215a3c..2d6788eb 100644 --- a/rust/src/database_models.rs +++ b/rust/src/database_models.rs @@ -125,6 +125,18 @@ 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, @@ -146,6 +158,17 @@ 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, @@ -184,6 +207,17 @@ pub struct FileRevRow {      pub mimetype: Option<String>,  } +#[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)] @@ -210,6 +244,29 @@ 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, | 
