From 6ebcc10405f636e0016c54c5b604d0366ae24380 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Sat, 6 Jun 2026 00:15:19 +0200 Subject: [PATCH] feat(server): 'seed' subcommand wiring the Spectrum cataloguing seed (#14) --- crates/server/src/lib.rs | 25 +++++++++++++++++++++++++ crates/server/src/main.rs | 5 ++++- crates/server/tests/seed.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 crates/server/tests/seed.rs diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 760668e..58249f4 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -117,6 +117,31 @@ pub mod test_support { } } +/// One-shot: apply migrations (idempotent), then seed the baseline Spectrum cataloguing +/// vocabularies + field definitions. Safe to re-run (the seed is idempotent). +pub async fn seed(database_url: &str) -> anyhow::Result<()> { + // CLI one-shot: a tiny pool is plenty. + let db = Db::connect(database_url, 2) + .await + .context("connecting to the database")?; + + // Apply migrations first so `server seed` works on a fresh DB without first + // starting the server. Migrations are idempotent. + db.migrate().await.context("running database migrations")?; + + let mut tx = db.pool().begin().await?; + + db::seed::seed_spectrum_cataloguing(&mut tx) + .await + .context("seeding Spectrum cataloguing baseline")?; + + tx.commit().await?; + + println!("seeded Spectrum cataloguing baseline (idempotent)"); + + Ok(()) +} + /// Create a user from the CLI (admin bootstrap). Opens its own connection (CLI /// one-shot); reads the password from the `BOOTSTRAP_PASSWORD` env var if set, /// otherwise prompts (hidden input). The plaintext is not zeroized, but it is diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 69c36eb..1894aa3 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -1,6 +1,6 @@ use clap::{Parser, Subcommand, ValueEnum}; use domain::Role; -use server::{Config, create_user, run}; +use server::{Config, create_user, run, seed}; #[derive(Parser)] #[command(version, about = "Collection management system server")] @@ -20,6 +20,8 @@ enum Command { #[arg(long, value_enum)] role: RoleArg, }, + /// Seed the baseline Spectrum cataloguing vocabularies + field definitions (idempotent). + Seed, } #[derive(Clone, Copy, ValueEnum)] @@ -50,5 +52,6 @@ async fn main() -> anyhow::Result<()> { Some(Command::CreateUser { email, role }) => { create_user(&cli.config.database_url, &email, role.into()).await } + Some(Command::Seed) => seed(&cli.config.database_url).await, } } diff --git a/crates/server/tests/seed.rs b/crates/server/tests/seed.rs new file mode 100644 index 0000000..a793fcd --- /dev/null +++ b/crates/server/tests/seed.rs @@ -0,0 +1,33 @@ +use db::{Db, fields, seed, vocab}; +use sqlx::PgPool; + +// Note: `server::seed` opens its own DB connection by URL, but `#[sqlx::test]` +// provisions a temporary database whose URL is not directly exposed. This test +// exercises the building block the command composes — `db::seed::seed_spectrum_cataloguing` +// — against the test pool, run twice to prove the idempotency the command relies on. +#[sqlx::test(migrations = "../db/migrations")] +async fn seed_is_idempotent_via_building_block(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(); + } + + // A representative seeded vocabulary and field definition are present after two runs. + assert!( + vocab::vocabulary_by_key(db.pool(), "material") + .await + .unwrap() + .is_some(), + "vocabulary 'material' should be seeded" + ); + assert!( + fields::field_definition_by_key(db.pool(), "title") + .await + .unwrap() + .is_some(), + "field definition 'title' should be seeded" + ); +}