feat(db): seed a representative Spectrum cataloguing field set (idempotent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 11:20:35 +02:00
parent 91a9eb2964
commit adc7c61ee2
3 changed files with 253 additions and 0 deletions
+1
View File
@@ -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};
+160
View File
@@ -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<VocabularyId, sqlx::Error> {
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(),
}
}