diff --git a/CHANGELOG.md b/CHANGELOG.md index ce9153ec..d2ec107c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + - [747](https://github.com/thoth-pub/thoth/pull/747) - Add `checksum` and `checksum_algorithm` fields to `Location` ## [[1.1.1]](https://github.com/thoth-pub/thoth/releases/tag/v1.1.1) - 2026-04-24 ### Security diff --git a/thoth-api/migrations/20260429_v1.2.0/down.sql b/thoth-api/migrations/20260429_v1.2.0/down.sql new file mode 100644 index 00000000..296c3083 --- /dev/null +++ b/thoth-api/migrations/20260429_v1.2.0/down.sql @@ -0,0 +1,6 @@ +ALTER TABLE public.location + DROP CONSTRAINT IF EXISTS location_checksum_and_algorithm_all_or_none, + DROP COLUMN IF EXISTS checksum, + DROP COLUMN IF EXISTS checksum_algorithm; + +DROP TYPE IF EXISTS public.checksum_algorithm; diff --git a/thoth-api/migrations/20260429_v1.2.0/up.sql b/thoth-api/migrations/20260429_v1.2.0/up.sql new file mode 100644 index 00000000..389adf54 --- /dev/null +++ b/thoth-api/migrations/20260429_v1.2.0/up.sql @@ -0,0 +1,10 @@ +CREATE TYPE public.checksum_algorithm AS ENUM ( + 'MD5', + 'SHA256', + 'SHA1' +); + +ALTER TABLE public.location + ADD COLUMN checksum TEXT, + ADD COLUMN checksum_algorithm public.checksum_algorithm, + ADD CONSTRAINT location_checksum_and_algorithm_all_or_none CHECK ((checksum IS NULL AND checksum_algorithm IS NULL) OR (checksum IS NOT NULL AND checksum_algorithm IS NOT NULL)); diff --git a/thoth-api/src/graphql/model.rs b/thoth-api/src/graphql/model.rs index 974e8762..3c09f228 100644 --- a/thoth-api/src/graphql/model.rs +++ b/thoth-api/src/graphql/model.rs @@ -21,7 +21,7 @@ use crate::model::{ contribution::{Contribution, ContributionType}, contributor::Contributor, endorsement::{Endorsement, EndorsementOrderBy}, - file::{File, FileType}, + file::{ChecksumAlgorithm, File, FileType}, funding::Funding, imprint::{Imprint, ImprintField, ImprintOrderBy}, institution::Institution, @@ -1916,6 +1916,16 @@ impl Location { self.canonical } + #[graphql(description = "Checksum of the full text file as returned by the platform")] + pub fn checksum(&self) -> Option<&String> { + self.checksum.as_ref() + } + + #[graphql(description = "Algorithm used to generate the checksum (MD5, SHA-256 or SHA-1)")] + pub fn checksum_algorithm(&self) -> Option<&ChecksumAlgorithm> { + self.checksum_algorithm.as_ref() + } + #[graphql(description = "Date and time at which the location record was created")] pub fn created_at(&self) -> Timestamp { self.created_at diff --git a/thoth-api/src/graphql/mutation.rs b/thoth-api/src/graphql/mutation.rs index fe698e58..33086dbf 100644 --- a/thoth-api/src/graphql/mutation.rs +++ b/thoth-api/src/graphql/mutation.rs @@ -1488,7 +1488,13 @@ impl MutationRoot { &mime_type, bytes, )?; - file_upload.sync_related_metadata(context, &work, &cdn_url, featured_video_dimensions)?; + file_upload.sync_related_metadata( + context, + &work, + &cdn_url, + &file.sha256, + featured_video_dimensions, + )?; reconcile_replaced_object( s3_client, diff --git a/thoth-api/src/graphql/tests.rs b/thoth-api/src/graphql/tests.rs index 4965025a..f697da4b 100644 --- a/thoth-api/src/graphql/tests.rs +++ b/thoth-api/src/graphql/tests.rs @@ -470,6 +470,8 @@ fn make_new_location(publication_id: Uuid, canonical: bool) -> NewLocation { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Other, canonical, + checksum: None, + checksum_algorithm: None, } } @@ -1104,6 +1106,8 @@ fn patch_location(location: &Location) -> PatchLocation { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, } } diff --git a/thoth-api/src/model/file/crud.rs b/thoth-api/src/model/file/crud.rs index 251b20b1..1c5789a0 100644 --- a/thoth-api/src/model/file/crud.rs +++ b/thoth-api/src/model/file/crud.rs @@ -1,7 +1,6 @@ -use super::FileType; use super::{ - upload_request_headers, File, FileCleanupCandidate, FilePolicy, FileUpload, FileUploadResponse, - NewFile, NewFileUpload, + upload_request_headers, ChecksumAlgorithm, File, FileCleanupCandidate, FilePolicy, FileType, + FileUpload, FileUploadResponse, NewFile, NewFileUpload, }; use crate::db::PgPool; use crate::model::{ @@ -724,6 +723,7 @@ impl FileUpload { ctx: &C, work: &Work, cdn_url: &str, + cdn_sha256: &str, featured_video_dimensions: Option<(i32, i32)>, ) -> ThothResult<()> { match self.file_type { @@ -741,6 +741,7 @@ impl FileUpload { publication_id, work.landing_page.clone(), cdn_url, + Some(cdn_sha256.to_string()), )?; } FileType::AdditionalResource => { @@ -792,6 +793,7 @@ impl FileUpload { publication_id: Uuid, landing_page: Option, full_text_url: &str, + sha256: Option, ) -> ThothResult<()> { use crate::schema::location::dsl; @@ -809,6 +811,8 @@ impl FileUpload { patch.full_text_url = Some(full_text_url.to_string()); patch.landing_page = landing_page; patch.canonical = true; + patch.checksum = sha256; + patch.checksum_algorithm = Some(ChecksumAlgorithm::Sha256); if patch.canonical { patch.canonical_record_complete(ctx.db())?; } @@ -830,6 +834,8 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: false, + checksum: sha256, + checksum_algorithm: Some(ChecksumAlgorithm::Sha256), }; let created_location = Location::create(ctx.db(), &new_location)?; let mut patch = PatchLocation::from(created_location.clone()); @@ -845,6 +851,8 @@ impl FileUpload { full_text_url: Some(full_text_url.to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: sha256, + checksum_algorithm: Some(ChecksumAlgorithm::Sha256), }; new_location.canonical_record_complete(ctx.db())?; Location::create(ctx.db(), &new_location)?; diff --git a/thoth-api/src/model/file/mod.rs b/thoth-api/src/model/file/mod.rs index 5e83912d..c45ce2a7 100644 --- a/thoth-api/src/model/file/mod.rs +++ b/thoth-api/src/model/file/mod.rs @@ -50,6 +50,24 @@ pub enum FileType { WorkFeaturedVideo, } +#[cfg_attr( + feature = "backend", + derive(diesel_derive_enum::DbEnum, juniper::GraphQLEnum), + graphql(description = "Algorithm used to create file checksum"), + ExistingTypePath = "crate::schema::sql_types::ChecksumAlgorithm" +)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, EnumString, Display)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum ChecksumAlgorithm { + #[cfg_attr(feature = "backend", db_rename = "MD5")] + Md5, + #[cfg_attr(feature = "backend", db_rename = "SHA256")] + Sha256, + #[cfg_attr(feature = "backend", db_rename = "SHA1")] + Sha1, +} + #[cfg_attr(feature = "backend", derive(diesel::Queryable))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] diff --git a/thoth-api/src/model/file/tests.rs b/thoth-api/src/model/file/tests.rs index 04844c8d..70d24946 100644 --- a/thoth-api/src/model/file/tests.rs +++ b/thoth-api/src/model/file/tests.rs @@ -1243,7 +1243,7 @@ mod crud { let cover_url = "https://cdn.example.org/10.1234/abc/def_frontcover.jpg"; upload - .sync_related_metadata(&ctx, &work, cover_url, None) + .sync_related_metadata(&ctx, &work, cover_url, "checksum", None) .expect("Failed to sync frontcover metadata"); let refreshed_work = Work::from_id(pool.as_ref(), &work.work_id) @@ -1284,7 +1284,7 @@ mod crud { let video_url = "https://cdn.example.org/10.1234/abc/def/resources/video.mp4"; upload - .sync_related_metadata(&ctx, &work, video_url, Some((1280, 720))) + .sync_related_metadata(&ctx, &work, video_url, "checksum", Some((1280, 720))) .expect("Failed to sync featured-video metadata"); let refreshed = crate::model::work_featured_video::WorkFeaturedVideo::from_id( diff --git a/thoth-api/src/model/location/crud.rs b/thoth-api/src/model/location/crud.rs index a91e6723..44ace766 100644 --- a/thoth-api/src/model/location/crud.rs +++ b/thoth-api/src/model/location/crud.rs @@ -64,6 +64,12 @@ impl Crud for Location { LocationField::Canonical => { apply_directional_order!(query, order.direction, order, canonical) } + LocationField::Checksum => { + apply_directional_order!(query, order.direction, order, checksum) + } + LocationField::ChecksumAlgorithm => { + apply_directional_order!(query, order.direction, order, checksum_algorithm) + } LocationField::CreatedAt => { apply_directional_order!(query, order.direction, order, created_at) } diff --git a/thoth-api/src/model/location/mod.rs b/thoth-api/src/model/location/mod.rs index 8dff1152..c35a25cd 100644 --- a/thoth-api/src/model/location/mod.rs +++ b/thoth-api/src/model/location/mod.rs @@ -4,7 +4,7 @@ use strum::EnumString; use uuid::Uuid; use crate::graphql::types::inputs::Direction; -use crate::model::Timestamp; +use crate::model::{file::ChecksumAlgorithm, Timestamp}; #[cfg(feature = "backend")] use crate::schema::location; #[cfg(feature = "backend")] @@ -165,6 +165,8 @@ pub enum LocationField { FullTextUrl, LocationPlatform, Canonical, + Checksum, + ChecksumAlgorithm, CreatedAt, UpdatedAt, } @@ -179,6 +181,8 @@ pub struct Location { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, + pub checksum_algorithm: Option, pub created_at: Timestamp, pub updated_at: Timestamp, } @@ -195,6 +199,8 @@ pub struct NewLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, + pub checksum_algorithm: Option, } #[cfg_attr( @@ -210,6 +216,8 @@ pub struct PatchLocation { pub full_text_url: Option, pub location_platform: LocationPlatform, pub canonical: bool, + pub checksum: Option, + pub checksum_algorithm: Option, } #[cfg_attr(feature = "backend", derive(diesel::Queryable))] @@ -260,6 +268,8 @@ impl From for PatchLocation { full_text_url: location.full_text_url, location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum, + checksum_algorithm: location.checksum_algorithm, } } } diff --git a/thoth-api/src/model/location/policy.rs b/thoth-api/src/model/location/policy.rs index 151799a6..a55ffecd 100644 --- a/thoth-api/src/model/location/policy.rs +++ b/thoth-api/src/model/location/policy.rs @@ -39,6 +39,11 @@ impl CreatePolicy for LocationPolicy { return Err(ThothError::ThothLocationError); } + // Only superusers can add a checksum. + if !user.is_superuser() && data.checksum.is_some() { + return Err(ThothError::CreateLocationChecksumError); + } + // Canonical locations must be complete; non-canonical locations must satisfy rules. if data.canonical { data.canonical_record_complete(ctx.db())?; @@ -74,6 +79,20 @@ impl UpdatePolicy for LocationPolicy { return Err(ThothError::ThothUpdateCanonicalError); } + // Only superusers can add a checksum. + if current.checksum.is_none() && patch.checksum.is_some() && !user.is_superuser() { + return Err(ThothError::UpdateLocationChecksumError); + } + + // Only superusers can update or delete an existing checksum. + if ((current.checksum.is_some() && current.checksum != patch.checksum) + || (current.checksum_algorithm.is_some() + && current.checksum_algorithm != patch.checksum_algorithm)) + && !user.is_superuser() + { + return Err(ThothError::UpdateLocationChecksumError); + } + // If setting canonical to true, require record completeness. if patch.canonical { patch.canonical_record_complete(ctx.db())?; diff --git a/thoth-api/src/model/location/tests.rs b/thoth-api/src/model/location/tests.rs index c87a1ee4..bafa8c2a 100644 --- a/thoth-api/src/model/location/tests.rs +++ b/thoth-api/src/model/location/tests.rs @@ -142,6 +142,8 @@ mod conversions { created_at: Default::default(), updated_at: Default::default(), canonical: true, + checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), }; let patch_location = PatchLocation::from(location.clone()); @@ -152,6 +154,7 @@ mod conversions { assert_eq!(patch_location.full_text_url, location.full_text_url); assert_eq!(patch_location.location_platform, location.location_platform); assert_eq!(patch_location.canonical, location.canonical); + assert_eq!(patch_location.checksum, location.checksum); } #[cfg(feature = "backend")] @@ -232,6 +235,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -242,6 +247,8 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -293,6 +300,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -304,6 +313,8 @@ mod policy { full_text_url: None, location_platform: location.location_platform, canonical: true, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -334,6 +345,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -345,6 +358,8 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; assert!(LocationPolicy::can_update(&ctx, &location, &patch, ()).is_ok()); @@ -374,6 +389,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical location"); @@ -384,6 +401,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, + checksum_algorithm: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_ok()); @@ -411,6 +430,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, + checksum_algorithm: None, }; let result = LocationPolicy::can_create(&ctx, &new_location, ()); @@ -438,6 +459,8 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, + checksum_algorithm: None, }; assert!(LocationPolicy::can_create(&ctx, &new_location, ()).is_err()); @@ -470,6 +493,8 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -481,6 +506,8 @@ mod policy { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: location.location_platform, canonical: location.canonical, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let update_result = LocationPolicy::can_update(&ctx, &location, &patch, ()); @@ -514,6 +541,8 @@ mod policy { full_text_url: Some("https://example.com/full".to_string()), location_platform: LocationPlatform::Thoth, canonical: true, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical thoth location"); @@ -526,6 +555,8 @@ mod policy { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create location"); @@ -537,11 +568,102 @@ mod policy { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: true, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); assert!(matches!(result, Err(ThothError::ThothUpdateCanonicalError))); } + + #[test] + fn crud_policy_rejects_non_superuser_checksum_create() { + let (_guard, pool) = setup_test_db(); + + let publisher = create_publisher(pool.as_ref()); + let org_id = publisher + .zitadel_id + .clone() + .expect("publisher missing zitadel id"); + let user = test_user_with_role("location-user", Role::PublisherUser, &org_id); + let ctx = test_context_with_user(pool.clone(), user); + + let imprint = create_imprint(pool.as_ref(), &publisher); + let work = create_work(pool.as_ref(), &imprint); + let publication = create_publication(pool.as_ref(), &work); + + let new_location = NewLocation { + publication_id: publication.publication_id, + landing_page: Some("https://example.com/landing".to_string()), + full_text_url: Some("https://example.com/full".to_string()), + location_platform: LocationPlatform::Other, + canonical: true, + checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), + }; + + let result = LocationPolicy::can_create(&ctx, &new_location, ()); + assert!(matches!( + result, + Err(ThothError::CreateLocationChecksumError) + )); + + let superuser = test_superuser("location-superuser"); + let super_ctx = test_context_with_user(pool.clone(), superuser); + assert!(LocationPolicy::can_create(&super_ctx, &new_location, ()).is_ok()); + } + + #[test] + fn crud_policy_rejects_non_superuser_checksum_update() { + let (_guard, pool) = setup_test_db(); + + let publisher = create_publisher(pool.as_ref()); + let org_id = publisher + .zitadel_id + .clone() + .expect("publisher missing zitadel id"); + let user = test_user_with_role("location-user", Role::PublisherUser, &org_id); + let ctx = test_context_with_user(pool.clone(), user); + + let imprint = create_imprint(pool.as_ref(), &publisher); + let work = create_work(pool.as_ref(), &imprint); + let publication = create_publication(pool.as_ref(), &work); + + let location = Location::create( + pool.as_ref(), + &NewLocation { + publication_id: publication.publication_id, + landing_page: Some("https://example.com/landing".to_string()), + full_text_url: Some("https://example.com/full".to_string()), + location_platform: LocationPlatform::Other, + canonical: false, + checksum: Some("examplechecksum".to_string()), + checksum_algorithm: Some(ChecksumAlgorithm::Md5), + }, + ) + .expect("Failed to create location"); + + let patch = PatchLocation { + location_id: location.location_id, + publication_id: location.publication_id, + landing_page: location.landing_page.clone(), + full_text_url: location.full_text_url.clone(), + location_platform: location.location_platform, + canonical: location.canonical, + checksum: Some("updatedchecksum".to_string()), + checksum_algorithm: location.checksum_algorithm, + }; + + let result = LocationPolicy::can_update(&ctx, &location, &patch, ()); + assert!(matches!( + result, + Err(ThothError::UpdateLocationChecksumError) + )); + + let superuser = test_superuser("location-superuser"); + let super_ctx = test_context_with_user(pool.clone(), superuser); + assert!(LocationPolicy::can_update(&super_ctx, &location, &patch, ()).is_ok()); + } } #[cfg(feature = "backend")] @@ -563,6 +685,8 @@ mod crud { location_platform: LocationPlatform, canonical: bool, landing_page: Option, + checksum: Option, + checksum_algorithm: Option, ) -> Location { let new_location = NewLocation { publication_id, @@ -570,6 +694,8 @@ mod crud { full_text_url: None, location_platform, canonical, + checksum, + checksum_algorithm, }; Location::create(pool, &new_location).expect("Failed to create location") @@ -590,6 +716,8 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }; let location = Location::create(pool.as_ref(), &new_location).expect("Failed to create"); @@ -604,6 +732,8 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::Other, canonical: true, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -629,6 +759,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some("https://example.com/landing".to_string()), + None, + None, ); let patch = PatchLocation { location_id: location.location_id, @@ -637,6 +769,8 @@ mod crud { full_text_url: location.full_text_url.clone(), location_platform: location.location_platform, canonical: false, + checksum: location.checksum.clone(), + checksum_algorithm: location.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -659,6 +793,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some("https://example.com/canonical".to_string()), + None, + None, ); let non_canonical = make_location( pool.as_ref(), @@ -666,6 +802,8 @@ mod crud { LocationPlatform::Other, false, Some("https://example.com/other".to_string()), + None, + None, ); let patch = PatchLocation { @@ -675,6 +813,8 @@ mod crud { full_text_url: non_canonical.full_text_url.clone(), location_platform: non_canonical.location_platform, canonical: true, + checksum: non_canonical.checksum.clone(), + checksum_algorithm: non_canonical.checksum_algorithm, }; let ctx = test_context(pool.clone(), "test-user"); @@ -709,6 +849,8 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: false, + checksum: None, + checksum_algorithm: None, }; let result = new_location.can_be_non_canonical(pool.as_ref()); @@ -732,6 +874,8 @@ mod crud { full_text_url: Some("https://example.com/full.pdf".to_string()), location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }, ) .expect("Failed to create canonical location"); @@ -742,6 +886,8 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::Other, canonical: false, + checksum: None, + checksum_algorithm: None, }; assert!(new_location.can_be_non_canonical(pool.as_ref()).is_ok()); @@ -783,6 +929,8 @@ mod crud { full_text_url: None, location_platform: LocationPlatform::PublisherWebsite, canonical: true, + checksum: None, + checksum_algorithm: None, }; let result = new_location.canonical_record_complete(pool.as_ref()); @@ -804,6 +952,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -811,6 +961,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let order = LocationOrderBy { @@ -872,6 +1024,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -879,6 +1033,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let count = Location::count(pool.as_ref(), None, vec![], vec![], vec![], None, None) @@ -901,6 +1057,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -908,6 +1066,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let count = Location::count( @@ -938,6 +1098,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -945,6 +1107,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let filtered = Location::all( @@ -987,6 +1151,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -994,6 +1160,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let filtered = Location::all( @@ -1033,6 +1201,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let other_publisher = create_publisher(pool.as_ref()); @@ -1045,6 +1215,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let filtered = Location::all( @@ -1085,6 +1257,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let second = make_location( pool.as_ref(), @@ -1092,6 +1266,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let mut ids = [first.location_id, second.location_id]; ids.sort(); @@ -1153,6 +1329,8 @@ mod crud { LocationPlatform::PublisherWebsite, true, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); make_location( pool.as_ref(), @@ -1160,6 +1338,8 @@ mod crud { LocationPlatform::Other, false, Some(format!("https://example.com/{}", Uuid::new_v4())), + None, + None, ); let fields: Vec LocationField> = vec![ diff --git a/thoth-api/src/schema.rs b/thoth-api/src/schema.rs index caa81647..160bd15a 100644 --- a/thoth-api/src/schema.rs +++ b/thoth-api/src/schema.rs @@ -82,6 +82,10 @@ pub mod sql_types { #[derive(diesel::sql_types::SqlType, diesel::query_builder::QueryId)] #[diesel(postgres_type(name = "accessibility_exception"))] pub struct AccessibilityException; + + #[derive(diesel::sql_types::SqlType, diesel::query_builder::QueryId)] + #[diesel(postgres_type(name = "checksum_algorithm"))] + pub struct ChecksumAlgorithm; } use diesel::{allow_tables_to_appear_in_same_query, joinable, table}; @@ -501,6 +505,7 @@ table! { table! { use diesel::sql_types::*; + use super::sql_types::ChecksumAlgorithm; use super::sql_types::LocationPlatform; location (location_id) { @@ -510,6 +515,8 @@ table! { full_text_url -> Nullable, location_platform -> LocationPlatform, canonical -> Bool, + checksum -> Nullable, + checksum_algorithm -> Nullable, created_at -> Timestamptz, updated_at -> Timestamptz, } diff --git a/thoth-errors/src/database_errors.rs b/thoth-errors/src/database_errors.rs index 5d61ad29..0a39835d 100644 --- a/thoth-errors/src/database_errors.rs +++ b/thoth-errors/src/database_errors.rs @@ -65,6 +65,7 @@ static DATABASE_CONSTRAINT_ERRORS: Map<&'static str, &'static str> = phf_map! { "issue_series_id_work_id_uniq" => "An issue on the selected series already exists for this work.", "issue_issue_ordinal_series_id_uniq" => "An issue with this ordinal number already exists.", "language_uniq_work_idx" => "Duplicate language code.", + "location_checksum_and_algorithm_all_or_none" => "Location checksum and checksum_algorithm must be provided together, or both must be empty.", "location_full_text_url_check" => "Invalid URL.", "location_landing_page_check" => "Invalid URL.", "location_uniq_canonical_true_idx" => "A canonical location for this publication already exists.", diff --git a/thoth-errors/src/lib.rs b/thoth-errors/src/lib.rs index 08209329..7afbc0ed 100644 --- a/thoth-errors/src/lib.rs +++ b/thoth-errors/src/lib.rs @@ -162,6 +162,10 @@ pub enum ThothError { AdditionalResourceFileUploadMissingAdditionalResourceId, #[error("Work featured video file upload missing work_featured_video_id")] WorkFeaturedVideoFileUploadMissingWorkFeaturedVideoId, + #[error("Only superusers can add a Location Checksum.")] + CreateLocationChecksumError, + #[error("Only superusers can update or delete an existing Location Checksum.")] + UpdateLocationChecksumError, } impl ThothError {