summaryrefslogtreecommitdiffstats
path: root/rust/src/entity_crud.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/entity_crud.rs')
-rw-r--r--rust/src/entity_crud.rs2162
1 files changed, 2162 insertions, 0 deletions
diff --git a/rust/src/entity_crud.rs b/rust/src/entity_crud.rs
new file mode 100644
index 00000000..d5c8081b
--- /dev/null
+++ b/rust/src/entity_crud.rs
@@ -0,0 +1,2162 @@
+use crate::database_models::*;
+use crate::database_schema::*;
+use crate::editing::*;
+use crate::endpoint_handlers::get_release_files;
+use crate::errors::*;
+use crate::identifiers::*;
+use crate::server::*;
+use chrono;
+use diesel::prelude::*;
+use diesel::{self, insert_into};
+use fatcat_api_spec::models::*;
+use sha1::Sha1;
+use std::marker::Sized;
+use std::str::FromStr;
+use uuid::Uuid;
+
+/* 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
+ * db_get_edit
+ * db_delete_edit
+ * db_get_redirects
+ * db_accept_edits
+ *
+ * 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;
+
+ // Generic Methods
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self>;
+ fn db_get(conn: &DbConn, ident: FatCatId, hide: HideFlags) -> Result<Self>;
+ fn db_get_rev(conn: &DbConn, rev_id: Uuid, hide: HideFlags) -> Result<Self>;
+ fn db_expand(&mut self, conn: &DbConn, expand: ExpandFlags) -> Result<()>;
+ 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>>;
+ fn db_get_edit(conn: &DbConn, edit_id: Uuid) -> Result<Self::EditRow>;
+ fn db_delete_edit(conn: &DbConn, edit_id: Uuid) -> Result<()>;
+ fn db_get_redirects(conn: &DbConn, ident: FatCatId) -> Result<Vec<FatCatId>>;
+ fn db_accept_edits(conn: &DbConn, editgroup_id: FatCatId) -> Result<u64>;
+
+ // Entity-specific Methods
+ fn db_from_row(
+ conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ hide: HideFlags,
+ ) -> Result<Self>;
+ fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid>;
+ fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>>;
+}
+
+#[derive(Clone, Copy, PartialEq)]
+pub struct ExpandFlags {
+ pub files: bool,
+ pub filesets: bool,
+ pub webcaptures: bool,
+ pub container: bool,
+ pub releases: bool,
+ pub creators: bool,
+}
+
+impl FromStr for ExpandFlags {
+ type Err = Error;
+ fn from_str(param: &str) -> Result<ExpandFlags> {
+ let list: Vec<&str> = param.split_terminator(",").collect();
+ Ok(ExpandFlags::from_str_list(&list))
+ }
+}
+
+impl ExpandFlags {
+ pub fn from_str_list(list: &[&str]) -> ExpandFlags {
+ ExpandFlags {
+ files: list.contains(&"files"),
+ filesets: list.contains(&"filesets"),
+ webcaptures: list.contains(&"webcaptures"),
+ container: list.contains(&"container"),
+ releases: list.contains(&"releases"),
+ creators: list.contains(&"creators"),
+ }
+ }
+ pub fn none() -> ExpandFlags {
+ ExpandFlags {
+ files: false,
+ filesets: false,
+ webcaptures: false,
+ container: false,
+ releases: false,
+ creators: false,
+ }
+ }
+}
+
+#[test]
+fn test_expand_flags() {
+ assert!(ExpandFlags::from_str_list(&vec![]).files == false);
+ assert!(ExpandFlags::from_str_list(&vec!["files"]).files == true);
+ assert!(ExpandFlags::from_str_list(&vec!["file"]).files == false);
+ let all = ExpandFlags::from_str_list(&vec![
+ "files",
+ "filesets",
+ "webcaptures",
+ "container",
+ "other_thing",
+ "releases",
+ "creators",
+ ]);
+ assert!(
+ all == ExpandFlags {
+ files: true,
+ filesets: true,
+ webcaptures: true,
+ container: true,
+ releases: true,
+ creators: true
+ }
+ );
+ assert!(ExpandFlags::from_str("").unwrap().files == false);
+ assert!(ExpandFlags::from_str("files").unwrap().files == true);
+ assert!(ExpandFlags::from_str("something,,files").unwrap().files == true);
+ assert!(ExpandFlags::from_str("file").unwrap().files == false);
+ let all =
+ ExpandFlags::from_str("files,container,other_thing,releases,creators,filesets,webcaptures")
+ .unwrap();
+ assert!(
+ all == ExpandFlags {
+ files: true,
+ filesets: true,
+ webcaptures: true,
+ container: true,
+ releases: true,
+ creators: true
+ }
+ );
+}
+
+#[derive(Clone, Copy, PartialEq)]
+pub struct HideFlags {
+ // release
+ pub abstracts: bool,
+ pub refs: bool,
+ pub contribs: bool,
+ // fileset
+ pub manifest: bool,
+ // webcapture
+ pub cdx: bool,
+}
+
+impl FromStr for HideFlags {
+ type Err = Error;
+ fn from_str(param: &str) -> Result<HideFlags> {
+ let list: Vec<&str> = param.split_terminator(",").collect();
+ Ok(HideFlags::from_str_list(&list))
+ }
+}
+
+impl HideFlags {
+ pub fn from_str_list(list: &[&str]) -> HideFlags {
+ HideFlags {
+ abstracts: list.contains(&"abstracts"),
+ refs: list.contains(&"refs"),
+ contribs: list.contains(&"contribs"),
+ manifest: list.contains(&"contribs"),
+ cdx: list.contains(&"contribs"),
+ }
+ }
+ pub fn none() -> HideFlags {
+ HideFlags {
+ abstracts: false,
+ refs: false,
+ contribs: false,
+ manifest: false,
+ cdx: false,
+ }
+ }
+}
+
+#[test]
+fn test_hide_flags() {
+ assert!(HideFlags::from_str_list(&vec![]).abstracts == false);
+ assert!(HideFlags::from_str_list(&vec!["abstracts"]).abstracts == true);
+ assert!(HideFlags::from_str_list(&vec!["abstract"]).abstracts == false);
+ let all = HideFlags::from_str_list(&vec![
+ "abstracts",
+ "refs",
+ "other_thing",
+ "contribs",
+ "manifest",
+ "cdx",
+ ]);
+ assert!(
+ all == HideFlags {
+ abstracts: true,
+ refs: true,
+ contribs: true,
+ manifest: true,
+ cdx: true,
+ }
+ );
+ assert!(HideFlags::from_str("").unwrap().abstracts == false);
+ assert!(HideFlags::from_str("abstracts").unwrap().abstracts == true);
+ assert!(
+ HideFlags::from_str("something,,abstracts")
+ .unwrap()
+ .abstracts
+ == true
+ );
+ assert!(HideFlags::from_str("file").unwrap().abstracts == false);
+ let all = HideFlags::from_str("abstracts,cdx,refs,manifest,other_thing,contribs").unwrap();
+ assert!(
+ all == HideFlags {
+ abstracts: true,
+ refs: true,
+ contribs: true,
+ manifest: true,
+ cdx: true,
+ }
+ );
+}
+
+macro_rules! generic_db_get {
+ ($ident_table:ident, $rev_table:ident) => {
+ fn db_get(conn: &DbConn, ident: FatCatId, hide: HideFlags) -> Result<Self> {
+ let res: Option<(Self::IdentRow, Self::RevRow)> = $ident_table::table
+ .find(ident.to_uuid())
+ .inner_join($rev_table::table)
+ .first(conn)
+ .optional()?;
+
+ match res {
+ Some((ident, rev)) => {
+ Self::db_from_row(conn, rev, Some(ident), hide)
+ },
+ None => {
+ // return a stub (deleted) entity if it's just deleted state
+ let ident_row: Self::IdentRow = $ident_table::table.find(ident.to_uuid()).first(conn)?;
+ if ident_row.rev_id.is_none() {
+ Self::from_deleted_row(ident_row)
+ } else {
+ bail!("unexpected condition: entity ident/rev join failed, yet row isn't in deleted state")
+ }
+ },
+ }
+ }
+ };
+}
+
+macro_rules! generic_db_get_rev {
+ ($rev_table:ident) => {
+ fn db_get_rev(conn: &DbConn, rev_id: Uuid, hide: HideFlags) -> Result<Self> {
+ let rev = $rev_table::table.find(rev_id).first(conn)?;
+
+ Self::db_from_row(conn, rev, None, hide)
+ }
+ };
+}
+
+macro_rules! generic_db_expand {
+ () => {
+ fn db_expand(&mut self, _conn: &DbConn, _expand: ExpandFlags) -> Result<()> {
+ Ok(())
+ }
+ };
+}
+
+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> {
+ if self.redirect.is_some() {
+ return Err(ErrorKind::OtherBadRequest(
+ "can't create an entity that redirects from the start".to_string()).into());
+ }
+ 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>> {
+ if models.iter().any(|m| m.redirect.is_some()) {
+ return Err(ErrorKind::OtherBadRequest(
+ "can't create an entity that redirects from the start".to_string(),
+ )
+ .into());
+ }
+ 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)?;
+ let no_redirect: Option<Uuid> = None;
+ // TODO: is this actually true? or should we allow updates in the same editgroup?
+ if current.is_live != true {
+ return Err(ErrorKind::InvalidEntityStateTransform(
+ "can't update an entity that doesn't exist yet".to_string()).into());
+ }
+ // Don't set prev_rev if current status is redirect
+ let prev_rev = match current.redirect_id {
+ Some(_) => None,
+ None => current.rev_id,
+ };
+
+ if self.state.is_none() {
+
+ if Some(ident.to_string()) == self.redirect {
+ return Err(ErrorKind::OtherBadRequest(
+ "tried to redirect entity to itself".to_string()).into());
+ }
+ // special case: redirect to another entity
+ if let Some(ref redirect_ident) = self.redirect {
+ let redirect_ident = FatCatId::from_str(&redirect_ident)?.to_uuid();
+ if Some(redirect_ident) == current.redirect_id {
+ return Err(ErrorKind::OtherBadRequest(
+ "redundantly redirecting entity to it's current target currently isn't supported".to_string()).into());
+ }
+ // TODO: if we get a diesel not-found here, should be a special error response?
+ let target: Self::IdentRow = $ident_table::table.find(redirect_ident).first(conn)?;
+ if target.is_live != true {
+ // there is no race condition on this check because WIP -> is_live=true is
+ // a one-way operation
+ // XXX:
+ return Err(ErrorKind::OtherBadRequest(
+ "attempted to redirect to a WIP entity".to_string()).into());
+ }
+ // Note: there is a condition where the target is already a redirect, but we
+ // don't handle that here because the state of the redirect could change before
+ // we accept this editgroup
+ 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(target.rev_id),
+ $edit_table::redirect_id.eq(redirect_ident),
+ $edit_table::prev_rev.eq(prev_rev),
+ $edit_table::extra_json.eq(&self.edit_extra),
+ ))
+ .get_result(conn)?;
+ return Ok(edit)
+ }
+ // special case: revert to point to an existing revision
+ if let Some(ref rev_id) = self.revision {
+ let rev_id = Uuid::from_str(&rev_id)?;
+ if Some(rev_id) == current.rev_id {
+ return Err(ErrorKind::OtherBadRequest(
+ "reverted entity to it's current state; this isn't currently supported".to_string()).into());
+ }
+ 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::redirect_id.eq(no_redirect),
+ $edit_table::prev_rev.eq(prev_rev),
+ $edit_table::extra_json.eq(&self.edit_extra),
+ ))
+ .get_result(conn)?;
+ return Ok(edit)
+ }
+ }
+
+ // regular insert/update
+ 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::redirect_id.eq(no_redirect),
+ $edit_table::prev_rev.eq(prev_rev),
+ $edit_table::extra_json.eq(&self.edit_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 {
+ return Err(ErrorKind::InvalidEntityStateTransform(
+ "can't update an entity that doesn't exist yet; delete edit object instead"
+ .to_string(),
+ )
+ .into());
+ }
+ if current.state()? == EntityState::Deleted {
+ return Err(ErrorKind::InvalidEntityStateTransform(
+ "entity was already deleted".to_string(),
+ )
+ .into());
+ }
+ 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_get_edit {
+ ($edit_table:ident) => {
+ fn db_get_edit(conn: &DbConn, edit_id: Uuid) -> Result<Self::EditRow> {
+ Ok($edit_table::table.find(edit_id).first(conn)?)
+ }
+ };
+}
+
+macro_rules! generic_db_delete_edit {
+ ($edit_table:ident) => {
+ /// This method assumes the connection is already in a transaction
+ fn db_delete_edit(conn: &DbConn, edit_id: Uuid) -> Result<()> {
+ // ensure that edit hasn't been accepted
+ let accepted_rows: Vec<(EditgroupRow, ChangelogRow, Self::EditRow)> = editgroup::table
+ .inner_join(changelog::table)
+ .inner_join($edit_table::table)
+ .filter($edit_table::id.eq(edit_id))
+ .limit(1)
+ .get_results(conn)?;
+ if accepted_rows.len() != 0 {
+ return Err(ErrorKind::EditgroupAlreadyAccepted(
+ "attempted to delete an already accepted edit".to_string(),
+ )
+ .into());
+ }
+ diesel::delete($edit_table::table.filter($edit_table::id.eq(edit_id))).execute(conn)?;
+ Ok(())
+ }
+ };
+}
+
+macro_rules! generic_db_get_redirects {
+ ($ident_table:ident) => {
+ fn db_get_redirects(conn: &DbConn, ident: FatCatId) -> Result<Vec<FatCatId>> {
+ let res: Vec<Uuid> = $ident_table::table
+ .select($ident_table::id)
+ .filter($ident_table::redirect_id.eq(ident.to_uuid()))
+ .get_results(conn)?;
+ Ok(res.iter().map(|u| FatCatId::from_uuid(u)).collect())
+ }
+ };
+}
+
+/*
+// This would be the clean and efficient way, but see:
+// https://github.com/diesel-rs/diesel/issues/1478
+//
+ diesel::update(container_ident::table)
+ .inner_join(container_edit::table.on(
+ container_ident::id.eq(container_edit::ident_id)
+ ))
+ .filter(container_edit::editgroup_id.eq(editgroup_id))
+ .values((
+ container_ident::is_live.eq(true),
+ container_ident::rev_id.eq(container_edit::rev_id),
+ container_ident::redirect_id.eq(container_edit::redirect_id),
+ ))
+ .execute()?;
+
+// Was previously:
+
+ for entity in &["container", "creator", "file", "work", "release"] {
+ diesel::sql_query(format!(
+ "
+ UPDATE {entity}_ident
+ SET
+ is_live = true,
+ rev_id = {entity}_edit.rev_id,
+ redirect_id = {entity}_edit.redirect_id
+ FROM {entity}_edit
+ WHERE
+ {entity}_ident.id = {entity}_edit.ident_id
+ AND {entity}_edit.editgroup_id = $1",
+ entity = entity
+ )).bind::<diesel::sql_types::Uuid, _>(editgroup_id)
+ .execute(conn)?;
+*/
+
+// UPDATE FROM version: single query for many rows
+// Works with Postgres, not Cockroach
+#[allow(unused_macros)]
+macro_rules! generic_db_accept_edits_batch {
+ ($entity_name_str:expr, $ident_table:ident, $edit_table:ident) => {
+ fn db_accept_edits(conn: &DbConn, editgroup_id: FatCatId) -> Result<u64> {
+ // NOTE: the checks and redirects can be skipped for accepts that are all inserts
+ // (which I guess we only know for batch inserts with auto-accept?)
+
+ // assert that we aren't redirecting to anything which is a redirect already
+ let forward_recursive_redirects: i64 = $edit_table::table
+ .inner_join(
+ $ident_table::table
+ .on($edit_table::redirect_id.eq($ident_table::id.nullable())),
+ )
+ .filter($edit_table::redirect_id.is_not_null())
+ .filter($edit_table::editgroup_id.eq(&editgroup_id.to_uuid()))
+ .filter($ident_table::redirect_id.is_not_null())
+ .count()
+ .get_result(conn)?;
+ if forward_recursive_redirects != 0 {
+ // TODO: revert transaction?
+ return Err(ErrorKind::OtherBadRequest(
+ "one or more (forward) recurisve redirects".to_string(),
+ )
+ .into());
+ }
+
+ // assert that we aren't redirecting while something already redirects to us
+ let backward_recursive_redirects: i64 = $ident_table::table
+ .inner_join(
+ $edit_table::table
+ .on($ident_table::redirect_id.eq($edit_table::ident_id.nullable())),
+ )
+ .filter($ident_table::redirect_id.is_not_null())
+ .filter($edit_table::editgroup_id.eq(editgroup_id.to_uuid()))
+ .filter($edit_table::redirect_id.is_not_null())
+ .count()
+ .get_result(conn)?;
+ if backward_recursive_redirects != 0 {
+ // TODO: revert transaction?
+ return Err(ErrorKind::OtherBadRequest(
+ "one or more (backward) recurisve redirects".to_string(),
+ )
+ .into());
+ }
+
+ let count = diesel::sql_query(format!(
+ "
+ UPDATE {entity}_ident
+ SET
+ is_live = true,
+ rev_id = {entity}_edit.rev_id,
+ redirect_id = {entity}_edit.redirect_id
+ FROM {entity}_edit
+ WHERE
+ {entity}_ident.id = {entity}_edit.ident_id
+ AND {entity}_edit.editgroup_id = $1",
+ entity = $entity_name_str
+ ))
+ .bind::<diesel::sql_types::Uuid, _>(editgroup_id.to_uuid())
+ .execute(conn)?;
+
+ // update any/all redirects for updated entities
+ let _redir_count = diesel::sql_query(format!(
+ "
+ UPDATE {entity}_ident
+ SET
+ rev_id = {entity}_edit.rev_id
+ FROM {entity}_edit
+ WHERE
+ {entity}_ident.redirect_id = {entity}_edit.ident_id
+ AND {entity}_edit.editgroup_id = $1",
+ entity = $entity_name_str
+ ))
+ .bind::<diesel::sql_types::Uuid, _>(editgroup_id.to_uuid())
+ .execute(conn)?;
+ Ok(count as u64)
+ }
+ };
+}
+
+// UPDATE ROW version: single query per row
+// CockroachDB version (slow, single query per row)
+#[allow(unused_macros)]
+macro_rules! generic_db_accept_edits_each {
+ ($ident_table:ident, $edit_table:ident) => {
+ fn db_accept_edits(conn: &DbConn, editgroup_id: FatCatId) -> Result<u64> {
+ // 1. select edit rows (in sql)
+ let edit_rows: Vec<Self::EditRow> = $edit_table::table
+ .filter($edit_table::editgroup_id.eq(&editgroup_id.to_uuid()))
+ .get_results(conn)?;
+ // 2. create ident rows (in rust)
+ let ident_rows: Vec<Self::IdentRow> = edit_rows
+ .iter()
+ .map(|edit| Self::IdentRow {
+ id: edit.ident_id,
+ is_live: true,
+ rev_id: edit.rev_id,
+ redirect_id: edit.redirect_id,
+ })
+ .collect();
+ /*
+ // 3. upsert ident rows (in sql)
+ let count: u64 = diesel::insert_into($ident_table::table)
+ .values(ident_rows)
+ .on_conflict()
+ .do_update()
+ .set(ident_rows)
+ .execute(conn)?;
+ */
+ // 3. update every row individually
+ let count = ident_rows.len() as u64;
+ for row in ident_rows {
+ diesel::update(&row).set(&row).execute(conn)?;
+ }
+ Ok(count)
+ }
+ };
+}
+
+macro_rules! generic_db_insert_rev {
+ () => {
+ fn db_insert_rev(&self, conn: &DbConn) -> Result<Uuid> {
+ Self::db_insert_revs(conn, &[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_db_get!(container_ident, container_rev);
+ generic_db_get_rev!(container_rev);
+ generic_db_expand!();
+ 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_get_edit!(container_edit);
+ generic_db_delete_edit!(container_edit);
+ generic_db_get_redirects!(container_ident);
+ generic_db_accept_edits_batch!("container", container_ident, container_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(ContainerEntity {
+ issnl: None,
+ wikidata_qid: None,
+ publisher: None,
+ name: None,
+ abbrev: None,
+ coden: None,
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_from_row(
+ _conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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: Some(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,
+ edit_extra: 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)?;
+ }
+ }
+
+ if models.iter().any(|m| m.name.is_none()) {
+ return Err(ErrorKind::OtherBadRequest(
+ "name is required for all Container entities".to_string(),
+ )
+ .into());
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(container_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| ContainerRevNewRow {
+ name: model.name.clone().unwrap(), // unwrap checked above
+ 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_db_get!(creator_ident, creator_rev);
+ generic_db_get_rev!(creator_rev);
+ generic_db_expand!();
+ 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_get_edit!(creator_edit);
+ generic_db_delete_edit!(creator_edit);
+ generic_db_get_redirects!(creator_ident);
+ generic_db_accept_edits_batch!("creator", creator_ident, creator_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(CreatorEntity {
+ extra: None,
+ edit_extra: None,
+ display_name: None,
+ given_name: None,
+ surname: None,
+ orcid: None,
+ wikidata_qid: None,
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ })
+ }
+
+ fn db_from_row(
+ _conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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: Some(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,
+ extra: rev_row.extra_json,
+ edit_extra: 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.orcid {
+ check_orcid(extid)?;
+ }
+ if let Some(ref extid) = entity.wikidata_qid {
+ check_wikidata_qid(extid)?;
+ }
+ }
+
+ if models.iter().any(|m| m.display_name.is_none()) {
+ return Err(ErrorKind::OtherBadRequest(
+ "display_name is required for all Creator entities".to_string(),
+ )
+ .into());
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(creator_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| CreatorRevNewRow {
+ display_name: model.display_name.clone().unwrap(), // unwrapped checked above
+ 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_db_get!(file_ident, file_rev);
+ generic_db_get_rev!(file_rev);
+ generic_db_expand!();
+ 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_get_edit!(file_edit);
+ generic_db_delete_edit!(file_edit);
+ generic_db_get_redirects!(file_ident);
+ generic_db_accept_edits_batch!("file", file_ident, file_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(FileEntity {
+ sha1: None,
+ sha256: None,
+ md5: None,
+ size: None,
+ urls: None,
+ mimetype: None,
+ release_ids: None,
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_from_row(
+ conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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 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();
+
+ let release_ids: Vec<FatCatId> = file_rev_release::table
+ .filter(file_rev_release::file_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: FileRevReleaseRow| FatCatId::from_uuid(&r.target_release_ident_id))
+ .collect();
+
+ Ok(FileEntity {
+ sha1: rev_row.sha1,
+ sha256: rev_row.sha256,
+ md5: rev_row.md5,
+ size: rev_row.size_bytes.map(|v| v as i64),
+ urls: Some(urls),
+ mimetype: rev_row.mimetype,
+ release_ids: Some(release_ids.iter().map(|fcid| fcid.to_string()).collect()),
+ state: state,
+ ident: ident_id,
+ revision: Some(rev_row.id.to_string()),
+ redirect: redirect_id,
+ extra: rev_row.extra_json,
+ edit_extra: None,
+ })
+ }
+
+ fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> {
+ // first verify hash syntax
+ for entity in models {
+ if let Some(ref hash) = entity.md5 {
+ check_md5(hash)?;
+ }
+ if let Some(ref hash) = entity.sha1 {
+ check_sha1(hash)?;
+ }
+ if let Some(ref hash) = entity.sha256 {
+ check_sha256(hash)?;
+ }
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(file_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| FileRevNewRow {
+ size_bytes: 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_rev_release_rows: Vec<FileRevReleaseRow> = vec![];
+ let mut file_url_rows: Vec<FileRevUrlNewRow> = vec![];
+
+ for (model, rev_id) in models.iter().zip(rev_ids.iter()) {
+ match &model.release_ids {
+ None => (),
+ Some(release_list) => {
+ let these_release_rows: Result<Vec<FileRevReleaseRow>> = release_list
+ .iter()
+ .map(|r| {
+ Ok(FileRevReleaseRow {
+ file_rev: *rev_id,
+ target_release_ident_id: FatCatId::from_str(r)?.to_uuid(),
+ })
+ })
+ .collect();
+ file_rev_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,
+ rel: u.rel.clone(),
+ url: u.url.clone(),
+ })
+ .collect();
+ file_url_rows.extend(these_url_rows);
+ }
+ };
+ }
+
+ if !file_rev_release_rows.is_empty() {
+ insert_into(file_rev_release::table)
+ .values(file_rev_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 FilesetEntity {
+ type EditRow = FilesetEditRow;
+ type EditNewRow = FilesetEditNewRow;
+ type IdentRow = FilesetIdentRow;
+ type IdentNewRow = FilesetIdentNewRow;
+ type RevRow = FilesetRevRow;
+
+ generic_db_get!(fileset_ident, fileset_rev);
+ generic_db_get_rev!(fileset_rev);
+ generic_db_expand!();
+ generic_db_create!(fileset_ident, fileset_edit);
+ generic_db_create_batch!(fileset_ident, fileset_edit);
+ generic_db_update!(fileset_ident, fileset_edit);
+ generic_db_delete!(fileset_ident, fileset_edit);
+ generic_db_get_history!(fileset_edit);
+ generic_db_get_edit!(fileset_edit);
+ generic_db_delete_edit!(fileset_edit);
+ generic_db_get_redirects!(fileset_ident);
+ generic_db_accept_edits_batch!("fileset", fileset_ident, fileset_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(FilesetEntity {
+ manifest: None,
+ urls: None,
+ release_ids: None,
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_from_row(
+ conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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 manifest: Vec<FilesetEntityManifest> = fileset_rev_file::table
+ .filter(fileset_rev_file::fileset_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: FilesetRevFileRow| FilesetEntityManifest {
+ path: r.path_name,
+ size: r.size_bytes,
+ md5: r.md5,
+ sha1: r.sha1,
+ sha256: r.sha256,
+ extra: r.extra_json,
+ })
+ .collect();
+
+ let urls: Vec<FileEntityUrls> = fileset_rev_url::table
+ .filter(fileset_rev_url::fileset_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: FilesetRevUrlRow| FileEntityUrls {
+ rel: r.rel,
+ url: r.url,
+ })
+ .collect();
+
+ let release_ids: Vec<FatCatId> = fileset_rev_release::table
+ .filter(fileset_rev_release::fileset_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: FilesetRevReleaseRow| FatCatId::from_uuid(&r.target_release_ident_id))
+ .collect();
+
+ Ok(FilesetEntity {
+ manifest: Some(manifest),
+ urls: Some(urls),
+ release_ids: Some(release_ids.iter().map(|fcid| fcid.to_string()).collect()),
+ state: state,
+ ident: ident_id,
+ revision: Some(rev_row.id.to_string()),
+ redirect: redirect_id,
+ extra: rev_row.extra_json,
+ edit_extra: None,
+ })
+ }
+
+ fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> {
+ // first verify hash syntax
+ for entity in models {
+ if let Some(ref manifest) = entity.manifest {
+ for file in manifest {
+ if let Some(ref hash) = file.md5 {
+ check_md5(hash)?;
+ }
+ if let Some(ref hash) = file.sha1 {
+ check_sha1(hash)?;
+ }
+ if let Some(ref hash) = file.sha256 {
+ check_sha256(hash)?;
+ }
+ }
+ }
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(fileset_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| FilesetRevNewRow {
+ extra_json: model.extra.clone(),
+ })
+ .collect::<Vec<FilesetRevNewRow>>(),
+ )
+ .returning(fileset_rev::id)
+ .get_results(conn)?;
+
+ let mut fileset_file_rows: Vec<FilesetRevFileNewRow> = vec![];
+ let mut fileset_url_rows: Vec<FilesetRevUrlNewRow> = vec![];
+ let mut fileset_release_rows: Vec<FilesetRevReleaseRow> = vec![];
+
+ for (model, rev_id) in models.iter().zip(rev_ids.iter()) {
+ match &model.manifest {
+ None => (),
+ Some(file_list) => {
+ let these_file_rows: Vec<FilesetRevFileNewRow> = file_list
+ .into_iter()
+ .map(|f| FilesetRevFileNewRow {
+ fileset_rev: *rev_id,
+ path_name: f.path.clone(),
+ size_bytes: f.size,
+ md5: f.md5.clone(),
+ sha1: f.sha1.clone(),
+ sha256: f.sha256.clone(),
+ extra_json: f.extra.clone(),
+ })
+ .collect();
+ fileset_file_rows.extend(these_file_rows);
+ }
+ };
+
+ match &model.urls {
+ None => (),
+ Some(url_list) => {
+ let these_url_rows: Vec<FilesetRevUrlNewRow> = url_list
+ .into_iter()
+ .map(|u| FilesetRevUrlNewRow {
+ fileset_rev: *rev_id,
+ rel: u.rel.clone(),
+ url: u.url.clone(),
+ })
+ .collect();
+ fileset_url_rows.extend(these_url_rows);
+ }
+ };
+
+ match &model.release_ids {
+ None => (),
+ Some(release_list) => {
+ let these_release_rows: Result<Vec<FilesetRevReleaseRow>> = release_list
+ .iter()
+ .map(|r| {
+ Ok(FilesetRevReleaseRow {
+ fileset_rev: *rev_id,
+ target_release_ident_id: FatCatId::from_str(r)?.to_uuid(),
+ })
+ })
+ .collect();
+ fileset_release_rows.extend(these_release_rows?);
+ }
+ };
+ }
+
+ if !fileset_file_rows.is_empty() {
+ insert_into(fileset_rev_file::table)
+ .values(fileset_file_rows)
+ .execute(conn)?;
+ }
+
+ if !fileset_url_rows.is_empty() {
+ insert_into(fileset_rev_url::table)
+ .values(fileset_url_rows)
+ .execute(conn)?;
+ }
+
+ if !fileset_release_rows.is_empty() {
+ insert_into(fileset_rev_release::table)
+ .values(fileset_release_rows)
+ .execute(conn)?;
+ }
+
+ Ok(rev_ids)
+ }
+}
+
+impl EntityCrud for WebcaptureEntity {
+ type EditRow = WebcaptureEditRow;
+ type EditNewRow = WebcaptureEditNewRow;
+ type IdentRow = WebcaptureIdentRow;
+ type IdentNewRow = WebcaptureIdentNewRow;
+ type RevRow = WebcaptureRevRow;
+
+ generic_db_get!(webcapture_ident, webcapture_rev);
+ generic_db_get_rev!(webcapture_rev);
+ generic_db_expand!();
+ generic_db_create!(webcapture_ident, webcapture_edit);
+ generic_db_create_batch!(webcapture_ident, webcapture_edit);
+ generic_db_update!(webcapture_ident, webcapture_edit);
+ generic_db_delete!(webcapture_ident, webcapture_edit);
+ generic_db_get_history!(webcapture_edit);
+ generic_db_get_edit!(webcapture_edit);
+ generic_db_delete_edit!(webcapture_edit);
+ generic_db_get_redirects!(webcapture_ident);
+ generic_db_accept_edits_batch!("webcapture", webcapture_ident, webcapture_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(WebcaptureEntity {
+ cdx: None,
+ archive_urls: None,
+ original_url: None,
+ timestamp: None,
+ release_ids: None,
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_from_row(
+ conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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 cdx: Vec<WebcaptureEntityCdx> = webcapture_rev_cdx::table
+ .filter(webcapture_rev_cdx::webcapture_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|c: WebcaptureRevCdxRow| WebcaptureEntityCdx {
+ surt: c.surt,
+ timestamp: c.timestamp,
+ url: c.url,
+ mimetype: c.mimetype,
+ status_code: c.status_code,
+ sha1: c.sha1,
+ sha256: c.sha256,
+ })
+ .collect();
+
+ let archive_urls: Vec<WebcaptureEntityArchiveUrls> = webcapture_rev_url::table
+ .filter(webcapture_rev_url::webcapture_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: WebcaptureRevUrlRow| WebcaptureEntityArchiveUrls {
+ rel: r.rel,
+ url: r.url,
+ })
+ .collect();
+
+ let release_ids: Vec<FatCatId> = webcapture_rev_release::table
+ .filter(webcapture_rev_release::webcapture_rev.eq(rev_row.id))
+ .get_results(conn)?
+ .into_iter()
+ .map(|r: WebcaptureRevReleaseRow| FatCatId::from_uuid(&r.target_release_ident_id))
+ .collect();
+
+ Ok(WebcaptureEntity {
+ cdx: Some(cdx),
+ archive_urls: Some(archive_urls),
+ original_url: Some(rev_row.original_url),
+ timestamp: Some(chrono::DateTime::from_utc(rev_row.timestamp, chrono::Utc)),
+ release_ids: Some(release_ids.iter().map(|fcid| fcid.to_string()).collect()),
+ state: state,
+ ident: ident_id,
+ revision: Some(rev_row.id.to_string()),
+ redirect: redirect_id,
+ extra: rev_row.extra_json,
+ edit_extra: None,
+ })
+ }
+
+ fn db_insert_revs(conn: &DbConn, models: &[&Self]) -> Result<Vec<Uuid>> {
+ // first verify hash syntax, and presence of required fields
+ for entity in models {
+ if let Some(ref cdx) = entity.cdx {
+ for row in cdx {
+ check_sha1(&row.sha1)?;
+ if let Some(ref hash) = row.sha256 {
+ check_sha256(hash)?;
+ }
+ }
+ }
+ if entity.timestamp.is_none() || entity.original_url.is_none() {
+ return Err(ErrorKind::OtherBadRequest(
+ "timestamp and original_url are required for webcapture entities".to_string(),
+ )
+ .into());
+ }
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(webcapture_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| WebcaptureRevNewRow {
+ // these unwraps safe because of check above
+ original_url: model.original_url.clone().unwrap(),
+ timestamp: model.timestamp.unwrap().naive_utc(),
+ extra_json: model.extra.clone(),
+ })
+ .collect::<Vec<WebcaptureRevNewRow>>(),
+ )
+ .returning(webcapture_rev::id)
+ .get_results(conn)?;
+
+ let mut webcapture_cdx_rows: Vec<WebcaptureRevCdxNewRow> = vec![];
+ let mut webcapture_url_rows: Vec<WebcaptureRevUrlNewRow> = vec![];
+ let mut webcapture_release_rows: Vec<WebcaptureRevReleaseRow> = vec![];
+
+ for (model, rev_id) in models.iter().zip(rev_ids.iter()) {
+ match &model.cdx {
+ None => (),
+ Some(cdx_list) => {
+ let these_cdx_rows: Vec<WebcaptureRevCdxNewRow> = cdx_list
+ .into_iter()
+ .map(|c| WebcaptureRevCdxNewRow {
+ webcapture_rev: *rev_id,
+ surt: c.surt.clone(),
+ timestamp: c.timestamp.clone(),
+ url: c.url.clone(),
+ mimetype: c.mimetype.clone(),
+ status_code: c.status_code,
+ sha1: c.sha1.clone(),
+ sha256: c.sha256.clone(),
+ })
+ .collect();
+ webcapture_cdx_rows.extend(these_cdx_rows);
+ }
+ };
+
+ match &model.archive_urls {
+ None => (),
+ Some(url_list) => {
+ let these_url_rows: Vec<WebcaptureRevUrlNewRow> = url_list
+ .into_iter()
+ .map(|u| WebcaptureRevUrlNewRow {
+ webcapture_rev: *rev_id,
+ rel: u.rel.clone(),
+ url: u.url.clone(),
+ })
+ .collect();
+ webcapture_url_rows.extend(these_url_rows);
+ }
+ };
+
+ match &model.release_ids {
+ None => (),
+ Some(release_list) => {
+ let these_release_rows: Result<Vec<WebcaptureRevReleaseRow>> = release_list
+ .iter()
+ .map(|r| {
+ Ok(WebcaptureRevReleaseRow {
+ webcapture_rev: *rev_id,
+ target_release_ident_id: FatCatId::from_str(r)?.to_uuid(),
+ })
+ })
+ .collect();
+ webcapture_release_rows.extend(these_release_rows?);
+ }
+ };
+ }
+
+ if !webcapture_cdx_rows.is_empty() {
+ insert_into(webcapture_rev_cdx::table)
+ .values(webcapture_cdx_rows)
+ .execute(conn)?;
+ }
+
+ if !webcapture_url_rows.is_empty() {
+ insert_into(webcapture_rev_url::table)
+ .values(webcapture_url_rows)
+ .execute(conn)?;
+ }
+
+ if !webcapture_release_rows.is_empty() {
+ insert_into(webcapture_rev_release::table)
+ .values(webcapture_release_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_db_get!(release_ident, release_rev);
+ generic_db_get_rev!(release_rev);
+ generic_db_update!(release_ident, release_edit);
+ generic_db_delete!(release_ident, release_edit);
+ generic_db_get_history!(release_edit);
+ generic_db_get_edit!(release_edit);
+ generic_db_delete_edit!(release_edit);
+ generic_db_get_redirects!(release_ident);
+ generic_db_accept_edits_batch!("release", release_ident, release_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(ReleaseEntity {
+ title: None,
+ release_type: None,
+ release_status: None,
+ release_date: None,
+ release_year: None,
+ doi: None,
+ pmid: None,
+ pmcid: None,
+ isbn13: None,
+ core_id: None,
+ wikidata_qid: None,
+ volume: None,
+ issue: None,
+ pages: None,
+ files: None,
+ filesets: None,
+ webcaptures: None,
+ container: None,
+ container_id: None,
+ publisher: None,
+ language: None,
+ work_id: None,
+ refs: None,
+ contribs: None,
+ abstracts: None,
+
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_expand(&mut self, conn: &DbConn, expand: ExpandFlags) -> Result<()> {
+ // Don't expand deleted entities
+ if self.state == Some("deleted".to_string()) {
+ return Ok(());
+ }
+ // TODO: should clarify behavior here. Would hit this path, eg, expanding files on a
+ // release revision (not ident). Should we fail (Bad Request), or silently just not include
+ // any files?
+ if expand.files && self.ident.is_some() {
+ let ident = match &self.ident {
+ None => bail!("Can't expand files on a non-concrete entity"), // redundant with above is_some()
+ Some(ident) => match &self.redirect {
+ // If we're a redirect, then expand for the *target* identifier, not *our*
+ // identifier. Tricky!
+ None => FatCatId::from_str(&ident)?,
+ Some(redir) => FatCatId::from_str(&redir)?,
+ },
+ };
+ self.files = Some(get_release_files(ident, HideFlags::none(), conn)?);
+ }
+ if expand.container {
+ if let Some(ref cid) = self.container_id {
+ self.container = Some(ContainerEntity::db_get(
+ conn,
+ FatCatId::from_str(&cid)?,
+ HideFlags::none(),
+ )?);
+ }
+ }
+ if expand.creators {
+ if let Some(ref mut contribs) = self.contribs {
+ for contrib in contribs {
+ if let Some(ref creator_id) = contrib.creator_id {
+ contrib.creator = Some(CreatorEntity::db_get(
+ conn,
+ FatCatId::from_str(creator_id)?,
+ HideFlags::none(),
+ )?);
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn db_create(&self, conn: &DbConn, edit_context: &EditContext) -> Result<Self::EditRow> {
+ if self.redirect.is_some() {
+ return Err(ErrorKind::OtherBadRequest(
+ "can't create an entity that redirects from the start".to_string(),
+ )
+ .into());
+ }
+ let mut edits = Self::db_create_batch(conn, edit_context, &[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)
+ if models.iter().any(|m| m.redirect.is_some()) {
+ return Err(ErrorKind::OtherBadRequest(
+ "can't create an entity that redirects from the start".to_string(),
+ )
+ .into());
+ }
+
+ // 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,
+ extra: None,
+ edit_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),
+ 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>,
+ hide: HideFlags,
+ ) -> 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: Option<Vec<ReleaseRef>> = match hide.refs {
+ true => None,
+ false => Some(
+ 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.map(|v| v as i64),
+ key: r.key,
+ extra: r.extra_json,
+ container_name: r.container_name,
+ year: r.year.map(|v| v as i64),
+ 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: Option<Vec<ReleaseContrib>> = match hide.contribs {
+ true => None,
+ false => Some(
+ 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.map(|v| v as i64),
+ 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: Option<Vec<ReleaseEntityAbstracts>> = match hide.abstracts {
+ true => None,
+ false => Some(
+ 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: Some(rev_row.title),
+ release_type: rev_row.release_type,
+ release_status: rev_row.release_status,
+ release_date: rev_row.release_date,
+ release_year: rev_row.release_year,
+ 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,
+ filesets: None,
+ webcaptures: 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: refs,
+ contribs: contribs,
+ abstracts: abstracts,
+ state: state,
+ ident: ident_id,
+ revision: Some(rev_row.id.to_string()),
+ redirect: redirect_id,
+ extra: rev_row.extra_json,
+ edit_extra: 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.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)?;
+ }
+ if let Some(ref release_type) = entity.release_type {
+ check_release_type(release_type)?;
+ }
+ if let Some(ref contribs) = entity.contribs {
+ for contrib in contribs {
+ if let Some(ref role) = contrib.role {
+ check_contrib_role(role)?;
+ }
+ }
+ }
+ }
+
+ if models.iter().any(|m| m.title.is_none()) {
+ return Err(ErrorKind::OtherBadRequest(
+ "title is required for all Release entities".to_string(),
+ )
+ .into());
+ }
+
+ let rev_ids: Vec<Uuid> = insert_into(release_rev::table)
+ .values(
+ models
+ .iter()
+ .map(|model| {
+ Ok(ReleaseRevNewRow {
+ title: model.title.clone().unwrap(), // titles checked above
+ release_type: model.release_type.clone(),
+ release_status: model.release_status.clone(),
+ release_date: model.release_date,
+ release_year: model.release_year,
+ 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.map(|v| v as i32),
+ key: r.key.clone(),
+ container_name: r.container_name.clone(),
+ year: r.year.map(|v| v as i32),
+ 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.map(|v| v as i32),
+ 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,
+ 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_db_get!(work_ident, work_rev);
+ generic_db_get_rev!(work_rev);
+ generic_db_expand!();
+ 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_get_edit!(work_edit);
+ generic_db_delete_edit!(work_edit);
+ generic_db_get_redirects!(work_ident);
+ generic_db_accept_edits_batch!("work", work_ident, work_edit);
+ generic_db_insert_rev!();
+
+ fn from_deleted_row(ident_row: Self::IdentRow) -> Result<Self> {
+ if ident_row.rev_id.is_some() {
+ bail!("called from_deleted_row with a non-deleted-state row")
+ }
+
+ Ok(WorkEntity {
+ state: Some(ident_row.state().unwrap().shortname()),
+ ident: Some(FatCatId::from_uuid(&ident_row.id).to_string()),
+ revision: ident_row.rev_id.map(|u| u.to_string()),
+ redirect: ident_row
+ .redirect_id
+ .map(|u| FatCatId::from_uuid(&u).to_string()),
+ extra: None,
+ edit_extra: None,
+ })
+ }
+
+ fn db_from_row(
+ _conn: &DbConn,
+ rev_row: Self::RevRow,
+ ident_row: Option<Self::IdentRow>,
+ _hide: HideFlags,
+ ) -> 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,
+ extra: rev_row.extra_json,
+ edit_extra: None,
+ })
+ }
+
+ 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)
+ }
+}