diff --git a/crates/db/src/lib.rs b/crates/db/src/lib.rs index 7e1475a..403e041 100644 --- a/crates/db/src/lib.rs +++ b/crates/db/src/lib.rs @@ -4,6 +4,7 @@ pub mod audit; pub mod authority; pub mod catalog; pub mod fields; +pub mod seed; pub mod vocab; use sqlx::postgres::{PgPool, PgPoolOptions}; diff --git a/crates/db/src/seed.rs b/crates/db/src/seed.rs new file mode 100644 index 0000000..dfb7c0b --- /dev/null +++ b/crates/db/src/seed.rs @@ -0,0 +1,160 @@ +//! Seed data: a representative subset of the Spectrum Cataloguing field set. +//! +//! Idempotent — each vocabulary and field definition is created only if a row with +//! that key does not already exist. Vocabularies are seeded empty; their terms are +//! populated by the organization or a later import. The inventory-minimum fields +//! (object number, name, location, …) live in the typed object core, not here. + +use domain::{AuthorityKind, FieldType, LocalizedLabel, NewFieldDefinition, VocabularyId}; + +use crate::{fields, vocab}; + +/// Seed the Spectrum cataloguing vocabularies and field definitions on `conn`. +/// Pass a transaction connection (`&mut *tx`) so the whole seed is atomic. +pub async fn seed_spectrum_cataloguing(conn: &mut sqlx::PgConnection) -> Result<(), sqlx::Error> { + let material = ensure_vocabulary(conn, "material").await?; + let object_name = ensure_vocabulary(conn, "object_name").await?; + let technique = ensure_vocabulary(conn, "technique").await?; + + let definitions = [ + def( + "object_type", + FieldType::Term { + vocabulary_id: object_name, + }, + "identification", + &[("sv", "Sakord"), ("en", "Object type")], + ), + def( + "title", + FieldType::LocalizedText, + "identification", + &[("sv", "Titel"), ("en", "Title")], + ), + def( + "comments", + FieldType::Text, + "identification", + &[("sv", "Kommentarer"), ("en", "Comments")], + ), + def( + "material", + FieldType::Term { + vocabulary_id: material, + }, + "description", + &[("sv", "Material"), ("en", "Material")], + ), + def( + "technique", + FieldType::Term { + vocabulary_id: technique, + }, + "description", + &[("sv", "Teknik"), ("en", "Technique")], + ), + def( + "physical_description", + FieldType::Text, + "description", + &[("sv", "Fysisk beskrivning"), ("en", "Physical description")], + ), + def( + "dimensions", + FieldType::Text, + "description", + &[("sv", "Mått"), ("en", "Dimensions")], + ), + def( + "inscription", + FieldType::Text, + "description", + &[("sv", "Inskription"), ("en", "Inscription")], + ), + def( + "content_description", + FieldType::Text, + "content", + &[ + ("sv", "Innehållsbeskrivning"), + ("en", "Content description"), + ], + ), + def( + "production_date", + FieldType::Date, + "production", + &[("sv", "Tillverkningsdatum"), ("en", "Production date")], + ), + def( + "production_place", + FieldType::Authority { + kind: Some(AuthorityKind::Place), + }, + "production", + &[("sv", "Tillverkningsplats"), ("en", "Production place")], + ), + def( + "production_person", + FieldType::Authority { + kind: Some(AuthorityKind::Person), + }, + "production", + &[("sv", "Tillverkare"), ("en", "Producer")], + ), + ]; + + for definition in &definitions { + ensure_field_definition(conn, definition).await?; + } + + Ok(()) +} + +/// Get-or-create a vocabulary by key, returning its id. +async fn ensure_vocabulary( + conn: &mut sqlx::PgConnection, + key: &str, +) -> Result { + if let Some(existing) = vocab::vocabulary_by_key(&mut *conn, key).await? { + Ok(existing.id) + } else { + Ok(vocab::create_vocabulary(&mut *conn, key).await?.id) + } +} + +/// Create a field definition only if its key is not already present. +async fn ensure_field_definition( + conn: &mut sqlx::PgConnection, + definition: &NewFieldDefinition, +) -> Result<(), sqlx::Error> { + if fields::field_definition_by_key(&mut *conn, &definition.key) + .await? + .is_none() + { + fields::create_field_definition(&mut *conn, definition).await?; + } + + Ok(()) +} + +fn def( + key: &str, + field_type: FieldType, + group: &str, + label_pairs: &[(&str, &str)], +) -> NewFieldDefinition { + NewFieldDefinition { + key: key.to_owned(), + field_type, + required: false, + group_key: Some(group.to_owned()), + labels: label_pairs + .iter() + .map(|(lang, label)| LocalizedLabel { + lang: (*lang).to_owned(), + label: (*label).to_owned(), + }) + .collect(), + } +} diff --git a/crates/db/tests/seed.rs b/crates/db/tests/seed.rs new file mode 100644 index 0000000..821927d --- /dev/null +++ b/crates/db/tests/seed.rs @@ -0,0 +1,92 @@ +use db::{Db, fields, seed, vocab}; +use domain::{AuthorityKind, FieldType}; +use sqlx::PgPool; + +#[sqlx::test] +async fn seed_creates_vocabularies_and_field_definitions(pool: PgPool) { + let db = Db::from_pool(pool); + + let mut tx = db.pool().begin().await.unwrap(); + seed::seed_spectrum_cataloguing(&mut tx).await.unwrap(); + tx.commit().await.unwrap(); + + for key in ["material", "object_name", "technique"] { + assert!( + vocab::vocabulary_by_key(db.pool(), key) + .await + .unwrap() + .is_some(), + "vocabulary {key} should be seeded" + ); + } + + let material_vocab = vocab::vocabulary_by_key(db.pool(), "material") + .await + .unwrap() + .unwrap(); + let material_field = fields::field_definition_by_key(db.pool(), "material") + .await + .unwrap() + .unwrap(); + assert_eq!( + material_field.field_type, + FieldType::Term { + vocabulary_id: material_vocab.id + } + ); + + let place = fields::field_definition_by_key(db.pool(), "production_place") + .await + .unwrap() + .unwrap(); + assert_eq!( + place.field_type, + FieldType::Authority { + kind: Some(AuthorityKind::Place) + } + ); + + let title = fields::field_definition_by_key(db.pool(), "title") + .await + .unwrap() + .unwrap(); + assert_eq!(title.field_type, FieldType::LocalizedText); + let date = fields::field_definition_by_key(db.pool(), "production_date") + .await + .unwrap() + .unwrap(); + assert_eq!(date.field_type, FieldType::Date); + + assert_eq!( + fields::list_field_definitions(db.pool()) + .await + .unwrap() + .len(), + 12 + ); +} + +#[sqlx::test] +async fn seed_is_idempotent(pool: PgPool) { + let db = Db::from_pool(pool); + + for _ in 0..2 { + let mut tx = db.pool().begin().await.unwrap(); + seed::seed_spectrum_cataloguing(&mut tx).await.unwrap(); + tx.commit().await.unwrap(); + } + + assert_eq!( + fields::list_field_definitions(db.pool()) + .await + .unwrap() + .len(), + 12 + ); + assert!( + vocab::vocabulary_by_key(db.pool(), "material") + .await + .unwrap() + .is_some() + ); +}