merge: wire Spectrum seed into runtime via 'server seed' (#14)
CI / web (push) Has been cancelled
CI / web (push) Has been cancelled
server seed subcommand (idempotent; migrates then seeds the baseline Spectrum cataloguing vocabularies + field definitions), just seed recipe, README step. Closes #14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,7 +49,14 @@ BOOTSTRAP_PASSWORD=changeme123 cargo run -p server -- create-user --email you@ex
|
||||
```
|
||||
Roles are `admin` or `editor`.
|
||||
|
||||
### 5. Run the web frontend
|
||||
### 5. Seed the baseline cataloguing fields (idempotent)
|
||||
```bash
|
||||
just seed # or: cargo run -p server -- seed
|
||||
```
|
||||
Populates the baseline Spectrum cataloguing vocabularies and field definitions. Safe to
|
||||
re-run — the seed is idempotent.
|
||||
|
||||
### 6. Run the web frontend
|
||||
The API server serves JSON only; in development the SPA is served by Vite, which proxies
|
||||
`/api` to `:8080`:
|
||||
```bash
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,10 @@ set dotenv-load
|
||||
run:
|
||||
cargo run -p server
|
||||
|
||||
# Seed the baseline Spectrum cataloguing vocabularies + field definitions (idempotent)
|
||||
seed:
|
||||
cargo run -p server -- seed
|
||||
|
||||
# Run the full test suite via cargo-nextest (per-test isolation, live output,
|
||||
# hang timeouts). nextest does not run doctests, so they run separately.
|
||||
test:
|
||||
|
||||
Reference in New Issue
Block a user