feat: edit/delete terms — audited, blocked when referenced (#30)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 18:43:02 +02:00
parent f6053068be
commit 09baf2949f
6 changed files with 542 additions and 3 deletions
+146 -2
View File
@@ -1,5 +1,7 @@
use db::{Db, vocab};
use domain::{AuditActor, LocalizedLabel, NewTerm};
use db::{Db, catalog, fields, vocab};
use domain::{
AuditActor, FieldType, LocalizedLabel, NewFieldDefinition, NewTerm, ObjectInput, Visibility,
};
use sqlx::PgPool;
#[sqlx::test]
@@ -169,3 +171,145 @@ async fn resolve_term_checks_vocabulary_membership(pool: PgPool) {
.is_none()
);
}
fn sample_object_input() -> ObjectInput {
ObjectInput {
object_number: "X.1".into(),
object_name: "Test".into(),
number_of_objects: 1,
brief_description: None,
current_location: None,
current_owner: None,
recorder: None,
recording_date: None,
visibility: Visibility::Draft,
}
}
#[sqlx::test(migrations = "../db/migrations")]
async fn update_term_changes_labels_and_uri(pool: PgPool) {
let db = Db::from_pool(pool);
let mut tx = db.pool().begin().await.unwrap();
let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
.await
.unwrap();
let term_id = vocab::add_term(
&mut tx,
AuditActor::System,
&NewTerm {
vocabulary_id: vocab.id,
external_uri: None,
labels: vec![LocalizedLabel {
lang: "sv".into(),
label: "Trä".into(),
}],
},
)
.await
.unwrap();
let existed = vocab::update_term(
&mut tx,
AuditActor::System,
vocab.id,
term_id,
Some("https://example.org/wood"),
&[LocalizedLabel {
lang: "sv".into(),
label: "Träslag".into(),
}],
)
.await
.unwrap();
assert!(existed);
tx.commit().await.unwrap();
let term = vocab::term_by_id(db.pool(), term_id)
.await
.unwrap()
.unwrap();
assert_eq!(
term.external_uri.as_deref(),
Some("https://example.org/wood")
);
assert_eq!(term.labels.len(), 1);
assert_eq!(term.labels[0].label, "Träslag");
}
#[sqlx::test(migrations = "../db/migrations")]
async fn delete_term_blocks_when_referenced_then_succeeds(pool: PgPool) {
use db::DeleteOutcome;
let db = Db::from_pool(pool);
let mut tx = db.pool().begin().await.unwrap();
let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
.await
.unwrap();
let term_id = vocab::add_term(
&mut tx,
AuditActor::System,
&NewTerm {
vocabulary_id: vocab.id,
external_uri: None,
labels: vec![LocalizedLabel {
lang: "sv".into(),
label: "Trä".into(),
}],
},
)
.await
.unwrap();
fields::create_field_definition(
&mut tx,
&NewFieldDefinition {
key: "material".into(),
field_type: FieldType::Term {
vocabulary_id: vocab.id,
},
required: false,
group_key: None,
labels: vec![LocalizedLabel {
lang: "sv".into(),
label: "Material".into(),
}],
},
)
.await
.unwrap();
let obj = catalog::create_object(&mut tx, AuditActor::System, &sample_object_input())
.await
.unwrap();
let mut map = serde_json::Map::new();
map.insert(
"material".into(),
serde_json::Value::String(term_id.to_string()),
);
catalog::set_object_fields(&mut tx, AuditActor::System, obj, &map)
.await
.unwrap();
let blocked = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
.await
.unwrap();
assert_eq!(blocked, DeleteOutcome::InUse { count: 1 });
catalog::set_object_fields(&mut tx, AuditActor::System, obj, &serde_json::Map::new())
.await
.unwrap();
let ok = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
.await
.unwrap();
assert_eq!(ok, DeleteOutcome::Deleted);
assert!(
vocab::term_by_id(&mut *tx, term_id)
.await
.unwrap()
.is_none()
);
let gone = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
.await
.unwrap();
assert_eq!(gone, DeleteOutcome::NotFound);
}