feat(server): 'seed' subcommand wiring the Spectrum cataloguing seed (#14)

This commit is contained in:
2026-06-06 00:15:19 +02:00
parent 325917a98e
commit 6ebcc10405
3 changed files with 62 additions and 1 deletions
+25
View File
@@ -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
+4 -1
View File
@@ -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,
}
}
+33
View File
@@ -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"
);
}