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:
@@ -4,6 +4,7 @@ pub mod audit;
|
|||||||
pub mod authority;
|
pub mod authority;
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod fields;
|
pub mod fields;
|
||||||
|
pub mod seed;
|
||||||
pub mod vocab;
|
pub mod vocab;
|
||||||
|
|
||||||
use sqlx::postgres::{PgPool, PgPoolOptions};
|
use sqlx::postgres::{PgPool, PgPoolOptions};
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user