Skip to content

Commit 6d69601

Browse files
authored
Merge pull request #748 from thoth-pub/feature/field-constraints
Feature/field constraints
2 parents 9412884 + 8e81a13 commit 6d69601

13 files changed

Lines changed: 162 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
- [749](https://github.com/thoth-pub/thoth/pull/749) - Correct locale code formatting in Crossref metadata output
1010
### Changed
1111
- [749](https://github.com/thoth-pub/thoth/pull/749) - Remove ISBN limit in Crossref metadata output (introduced in v0.8.7)
12+
- [748](https://github.com/thoth-pub/thoth/pull/748) - Require endorsement author names and featured video titles
1213

1314
## [[1.2.0]](https://github.com/thoth-pub/thoth/releases/tag/v1.2.0) - 2026-05-04
1415
### Added
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
ALTER TABLE endorsement
2+
DROP CONSTRAINT IF EXISTS endorsement_author_name_check,
3+
ALTER COLUMN author_name DROP NOT NULL;
4+
5+
ALTER TABLE work_featured_video
6+
DROP CONSTRAINT IF EXISTS work_featured_video_title_check,
7+
ALTER COLUMN title DROP NOT NULL;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
DO $$
2+
BEGIN
3+
IF EXISTS (
4+
SELECT 1
5+
FROM endorsement
6+
WHERE author_name IS NULL OR octet_length(author_name) < 1
7+
) THEN
8+
RAISE EXCEPTION 'Cannot make endorsement.author_name required: existing rows contain NULL or empty values';
9+
END IF;
10+
11+
IF EXISTS (
12+
SELECT 1
13+
FROM work_featured_video
14+
WHERE title IS NULL OR octet_length(title) < 1
15+
) THEN
16+
RAISE EXCEPTION 'Cannot make work_featured_video.title required: existing rows contain NULL or empty values';
17+
END IF;
18+
END $$;
19+
20+
ALTER TABLE endorsement
21+
ALTER COLUMN author_name SET NOT NULL,
22+
ADD CONSTRAINT endorsement_author_name_check CHECK (octet_length(author_name) >= 1);
23+
24+
ALTER TABLE work_featured_video
25+
ALTER COLUMN title SET NOT NULL,
26+
ADD CONSTRAINT work_featured_video_title_check CHECK (octet_length(title) >= 1);

thoth-api/src/graphql/model.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,8 +2419,8 @@ impl Endorsement {
24192419
}
24202420

24212421
#[graphql(description = "Name of the endorsement author")]
2422-
pub fn author_name(&self) -> Option<&String> {
2423-
self.author_name.as_ref()
2422+
pub fn author_name(&self) -> &String {
2423+
&self.author_name
24242424
}
24252425

24262426
#[graphql(description = "Role of the endorsement author")]
@@ -2682,8 +2682,8 @@ impl WorkFeaturedVideo {
26822682
}
26832683

26842684
#[graphql(description = "Title or caption of the featured video")]
2685-
pub fn title(&self) -> Option<&String> {
2686-
self.title.as_ref()
2685+
pub fn title(&self) -> &String {
2686+
&self.title
26872687
}
26882688

26892689
#[graphql(description = "CDN URL of the featured video")]

thoth-api/src/graphql/tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2587,7 +2587,7 @@ fn graphql_endorsement_supports_author_identity_fields() {
25872587
"endorsementId authorRole(markupFormat: PLAIN_TEXT) authorOrcid authorInstitutionId authorInstitution { institutionId ror } text(markupFormat: PLAIN_TEXT)",
25882588
NewEndorsement {
25892589
work_id: seed.book_work_id,
2590-
author_name: Some("Author".to_string()),
2590+
author_name: "Author".to_string(),
25912591
author_role: Some("*Visiting Scholar*".to_string()),
25922592
author_orcid: Some(Orcid::from_str("https://orcid.org/0000-0001-2345-6789").unwrap()),
25932593
author_institution_id: Some(institution.institution_id),
@@ -2644,7 +2644,7 @@ fn graphql_update_endorsement_supports_author_role_markup() {
26442644
"endorsementId authorRole(markupFormat: PLAIN_TEXT)",
26452645
NewEndorsement {
26462646
work_id: seed.book_work_id,
2647-
author_name: Some("Author".to_string()),
2647+
author_name: "Author".to_string(),
26482648
author_role: Some("Scholar".to_string()),
26492649
author_orcid: None,
26502650
author_institution_id: None,
@@ -2665,7 +2665,7 @@ fn graphql_update_endorsement_supports_author_role_markup() {
26652665
PatchEndorsement {
26662666
endorsement_id,
26672667
work_id: seed.book_work_id,
2668-
author_name: Some("Author".to_string()),
2668+
author_name: "Author".to_string(),
26692669
author_role: Some("*Lead Editor*".to_string()),
26702670
author_orcid: None,
26712671
author_institution_id: None,
@@ -2731,7 +2731,7 @@ fn graphql_review_and_endorsement_relations_null_after_institution_delete() {
27312731
pool.as_ref(),
27322732
&NewEndorsement {
27332733
work_id: seed.book_work_id,
2734-
author_name: Some("Author".to_string()),
2734+
author_name: "Author".to_string(),
27352735
author_role: Some("Scholar".to_string()),
27362736
author_orcid: Some(Orcid::from_str("https://orcid.org/0000-0001-2345-6789").unwrap()),
27372737
author_institution_id: Some(institution.institution_id),

thoth-api/src/model/endorsement/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum EndorsementField {
3333
pub struct Endorsement {
3434
pub endorsement_id: Uuid,
3535
pub work_id: Uuid,
36-
pub author_name: Option<String>,
36+
pub author_name: String,
3737
pub author_role: Option<String>,
3838
pub author_orcid: Option<Orcid>,
3939
pub author_institution_id: Option<Uuid>,
@@ -52,7 +52,7 @@ pub struct Endorsement {
5252
)]
5353
pub struct NewEndorsement {
5454
pub work_id: Uuid,
55-
pub author_name: Option<String>,
55+
pub author_name: String,
5656
pub author_role: Option<String>,
5757
pub author_orcid: Option<Orcid>,
5858
pub author_institution_id: Option<Uuid>,
@@ -70,7 +70,7 @@ pub struct NewEndorsement {
7070
pub struct PatchEndorsement {
7171
pub endorsement_id: Uuid,
7272
pub work_id: Uuid,
73-
pub author_name: Option<String>,
73+
pub author_name: String,
7474
pub author_role: Option<String>,
7575
pub author_orcid: Option<Orcid>,
7676
pub author_institution_id: Option<Uuid>,

thoth-api/src/model/endorsement/tests.rs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ fn make_endorsement(
77
pool: &crate::db::PgPool,
88
work_id: Uuid,
99
endorsement_ordinal: i32,
10-
author_name: Option<String>,
10+
author_name: String,
1111
) -> Endorsement {
1212
let data = NewEndorsement {
1313
work_id,
@@ -87,7 +87,7 @@ mod policy {
8787
let work = create_work(pool.as_ref(), &imprint);
8888
let data = NewEndorsement {
8989
work_id: work.work_id,
90-
author_name: Some("Author".to_string()),
90+
author_name: "Author".to_string(),
9191
author_role: Some("Role".to_string()),
9292
author_orcid: None,
9393
author_institution_id: None,
@@ -122,8 +122,7 @@ mod policy {
122122
let publisher = create_publisher(pool.as_ref());
123123
let imprint = create_imprint(pool.as_ref(), &publisher);
124124
let work = create_work(pool.as_ref(), &imprint);
125-
let endorsement =
126-
make_endorsement(pool.as_ref(), work.work_id, 1, Some("Author".to_string()));
125+
let endorsement = make_endorsement(pool.as_ref(), work.work_id, 1, "Author".to_string());
127126

128127
let patch = PatchEndorsement {
129128
endorsement_id: endorsement.endorsement_id,
@@ -142,7 +141,7 @@ mod policy {
142141

143142
let data = NewEndorsement {
144143
work_id: work.work_id,
145-
author_name: Some("Author".to_string()),
144+
author_name: "Author".to_string(),
146145
author_role: Some("Role".to_string()),
147146
author_orcid: None,
148147
author_institution_id: None,
@@ -208,7 +207,7 @@ mod policy {
208207

209208
let data = NewEndorsement {
210209
work_id: chapter.work_id,
211-
author_name: Some("Author".to_string()),
210+
author_name: "Author".to_string(),
212211
author_role: Some("Role".to_string()),
213212
author_orcid: None,
214213
author_institution_id: None,
@@ -244,7 +243,7 @@ mod crud {
244243

245244
let data = NewEndorsement {
246245
work_id: work.work_id,
247-
author_name: Some("Author".to_string()),
246+
author_name: "Author".to_string(),
248247
author_role: Some("Role".to_string()),
249248
author_orcid: Some(
250249
crate::model::Orcid::from_str("https://orcid.org/0000-0002-1234-5678").unwrap(),
@@ -280,6 +279,54 @@ mod crud {
280279
assert!(Endorsement::from_id(pool.as_ref(), &deleted.endorsement_id).is_err());
281280
}
282281

282+
#[test]
283+
fn crud_rejects_empty_author_name() {
284+
let (_guard, pool) = setup_test_db();
285+
286+
let publisher = create_publisher(pool.as_ref());
287+
let imprint = create_imprint(pool.as_ref(), &publisher);
288+
let work = create_work(pool.as_ref(), &imprint);
289+
290+
let data = NewEndorsement {
291+
work_id: work.work_id,
292+
author_name: "".to_string(),
293+
author_role: Some("Role".to_string()),
294+
author_orcid: None,
295+
author_institution_id: None,
296+
url: Some("https://example.com/endorsement".to_string()),
297+
text: Some("Endorsement text".to_string()),
298+
endorsement_ordinal: 1,
299+
};
300+
301+
let create_error = Endorsement::create(pool.as_ref(), &data).unwrap_err();
302+
assert!(matches!(
303+
create_error,
304+
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
305+
if msg.as_ref() == "Author name must not be an empty string."
306+
));
307+
308+
let endorsement = make_endorsement(pool.as_ref(), work.work_id, 1, "Author".to_string());
309+
let patch = PatchEndorsement {
310+
endorsement_id: endorsement.endorsement_id,
311+
work_id: endorsement.work_id,
312+
author_name: "".to_string(),
313+
author_role: endorsement.author_role.clone(),
314+
author_orcid: endorsement.author_orcid.clone(),
315+
author_institution_id: endorsement.author_institution_id,
316+
url: endorsement.url.clone(),
317+
text: endorsement.text.clone(),
318+
endorsement_ordinal: endorsement.endorsement_ordinal,
319+
};
320+
let ctx = test_context(pool.clone(), "test-user");
321+
322+
let update_error = endorsement.update(&ctx, &patch).unwrap_err();
323+
assert!(matches!(
324+
update_error,
325+
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
326+
if msg.as_ref() == "Author name must not be an empty string."
327+
));
328+
}
329+
283330
#[test]
284331
fn deleting_author_institution_nulls_relation() {
285332
let (_guard, pool) = setup_test_db();
@@ -293,7 +340,7 @@ mod crud {
293340
pool.as_ref(),
294341
&NewEndorsement {
295342
work_id: work.work_id,
296-
author_name: Some("Author".to_string()),
343+
author_name: "Author".to_string(),
297344
author_role: Some("Role".to_string()),
298345
author_orcid: Some(
299346
crate::model::Orcid::from_str("https://orcid.org/0000-0002-1234-5678").unwrap(),
@@ -324,8 +371,8 @@ mod crud {
324371
let imprint = create_imprint(pool.as_ref(), &publisher);
325372
let work = create_work(pool.as_ref(), &imprint);
326373

327-
let first = make_endorsement(pool.as_ref(), work.work_id, 1, Some("Author 1".to_string()));
328-
let second = make_endorsement(pool.as_ref(), work.work_id, 2, Some("Author 2".to_string()));
374+
let first = make_endorsement(pool.as_ref(), work.work_id, 1, "Author 1".to_string());
375+
let second = make_endorsement(pool.as_ref(), work.work_id, 2, "Author 2".to_string());
329376
let ctx = test_context(pool.clone(), "test-user");
330377

331378
let moved = second

thoth-api/src/model/file/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ fn create_work_featured_video(
141141

142142
let new_video = NewWorkFeaturedVideo {
143143
work_id,
144-
title: Some("Hosted video".to_string()),
144+
title: "Hosted video".to_string(),
145145
url: None,
146146
width: 560,
147147
height: 315,

thoth-api/src/model/work_featured_video/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum WorkFeaturedVideoField {
3333
pub struct WorkFeaturedVideo {
3434
pub work_featured_video_id: Uuid,
3535
pub work_id: Uuid,
36-
pub title: Option<String>,
36+
pub title: String,
3737
pub url: Option<String>,
3838
pub width: i32,
3939
pub height: i32,
@@ -49,7 +49,7 @@ pub struct WorkFeaturedVideo {
4949
)]
5050
pub struct NewWorkFeaturedVideo {
5151
pub work_id: Uuid,
52-
pub title: Option<String>,
52+
pub title: String,
5353
pub url: Option<String>,
5454
pub width: i32,
5555
pub height: i32,
@@ -64,7 +64,7 @@ pub struct NewWorkFeaturedVideo {
6464
pub struct PatchWorkFeaturedVideo {
6565
pub work_featured_video_id: Uuid,
6666
pub work_id: Uuid,
67-
pub title: Option<String>,
67+
pub title: String,
6868
pub url: Option<String>,
6969
pub width: i32,
7070
pub height: i32,

thoth-api/src/model/work_featured_video/tests.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ fn make_work_featured_video(
99
) -> WorkFeaturedVideo {
1010
let data = NewWorkFeaturedVideo {
1111
work_id,
12-
title: Some("Featured video".to_string()),
12+
title: "Featured video".to_string(),
1313
url,
1414
width: 560,
1515
height: 315,
@@ -80,7 +80,7 @@ mod policy {
8080
let work = create_work(pool.as_ref(), &imprint);
8181
let data = NewWorkFeaturedVideo {
8282
work_id: work.work_id,
83-
title: Some("Featured video".to_string()),
83+
title: "Featured video".to_string(),
8484
url: Some("https://cdn.example.org/video.mp4".to_string()),
8585
width: 560,
8686
height: 315,
@@ -128,7 +128,7 @@ mod policy {
128128

129129
let data = NewWorkFeaturedVideo {
130130
work_id: work.work_id,
131-
title: Some("Featured video".to_string()),
131+
title: "Featured video".to_string(),
132132
url: Some("https://cdn.example.org/video.mp4".to_string()),
133133
width: 560,
134134
height: 315,
@@ -190,7 +190,7 @@ mod policy {
190190

191191
let data = NewWorkFeaturedVideo {
192192
work_id: chapter.work_id,
193-
title: Some("Featured video".to_string()),
193+
title: "Featured video".to_string(),
194194
url: Some("https://cdn.example.org/video.mp4".to_string()),
195195
width: 560,
196196
height: 315,
@@ -231,7 +231,7 @@ mod crud {
231231
let patch = PatchWorkFeaturedVideo {
232232
work_featured_video_id: video.work_featured_video_id,
233233
work_id: video.work_id,
234-
title: Some("Updated featured video".to_string()),
234+
title: "Updated featured video".to_string(),
235235
url: Some("https://cdn.example.org/video-v2.mp4".to_string()),
236236
width: 640,
237237
height: 360,
@@ -247,6 +247,52 @@ mod crud {
247247
);
248248
}
249249

250+
#[test]
251+
fn crud_rejects_empty_title() {
252+
let (_guard, pool) = setup_test_db();
253+
254+
let publisher = create_publisher(pool.as_ref());
255+
let imprint = create_imprint(pool.as_ref(), &publisher);
256+
let work = create_work(pool.as_ref(), &imprint);
257+
258+
let data = NewWorkFeaturedVideo {
259+
work_id: work.work_id,
260+
title: "".to_string(),
261+
url: Some("https://cdn.example.org/video.mp4".to_string()),
262+
width: 560,
263+
height: 315,
264+
};
265+
266+
let create_error = WorkFeaturedVideo::create(pool.as_ref(), &data).unwrap_err();
267+
assert!(matches!(
268+
create_error,
269+
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
270+
if msg.as_ref() == "Featured video title must not be an empty string."
271+
));
272+
273+
let video = make_work_featured_video(
274+
pool.as_ref(),
275+
work.work_id,
276+
Some("https://cdn.example.org/video.mp4".to_string()),
277+
);
278+
let patch = PatchWorkFeaturedVideo {
279+
work_featured_video_id: video.work_featured_video_id,
280+
work_id: video.work_id,
281+
title: "".to_string(),
282+
url: video.url.clone(),
283+
width: video.width,
284+
height: video.height,
285+
};
286+
let ctx = test_context(pool.clone(), "test-user");
287+
288+
let update_error = video.update(&ctx, &patch).unwrap_err();
289+
assert!(matches!(
290+
update_error,
291+
thoth_errors::ThothError::DatabaseConstraintError(ref msg)
292+
if msg.as_ref() == "Featured video title must not be an empty string."
293+
));
294+
}
295+
250296
#[test]
251297
fn crud_from_work_id_returns_record() {
252298
let (_guard, pool) = setup_test_db();

0 commit comments

Comments
 (0)