feat: add --prune and a rolled-back --dry-run to bm:seed

This commit is contained in:
Jakub Zych
2026-10-02 17:28:07 +02:00
parent e8411adac3
commit 28f68d002f
6 changed files with 192 additions and 23 deletions

View File

@@ -21,7 +21,7 @@ Timestamps are stored in UTC (`TIMESTAMPTZ`) and written in RFC 3339 in Europe/W
- A material without `published_at` is a draft; a material whose `published_at` lies in the future is not shown yet.
- Cascades: deleting a course deletes its class sessions and readings and unlinks its deadlines and materials; deleting a meeting unlinks its sessions; deleting a reading or a user deletes the progress rows.
- Admin screens for every model in a "Studia BM" navigation group, guarded by three permissions.
- `bm:seed`: an idempotent upsert of meetings, courses, class sessions, deadlines and readings from a JSON file, all or nothing.
- `bm:seed`: an idempotent upsert of meetings, courses, class sessions, deadlines and readings from a JSON file, all or nothing, with `--prune` to delete what the file no longer lists and `--dry-run` to preview.
## API reference
@@ -183,10 +183,15 @@ Defaults in `config/config.yaml`, read as `jz.bm.<key>`:
```sh
./bin/bm bm:seed data/bm-2026-27.json
./bin/bm bm:seed data/bm-2026-27.json --dry-run
./bin/bm bm:seed data/bm-2026-27.json --dry-run --prune
./bin/bm bm:seed data/bm-2026-27.json --prune
```
Reads a JSON file, validates all of it, and upserts it in one transaction. Each kind is matched on its identity, updated in place when a field changed, and created otherwise; rows that are not in the file are left alone, so running the same file again changes nothing. Any error (an unknown key in the document, a bad value, a course key or meeting number that does not exist in the file) names the record and writes nothing. `--dry-run` only validates. The command prints a table of created, updated and unchanged rows per kind.
Reads a JSON file, validates all of it, and upserts it in one transaction. Each kind is matched on its identity, updated in place when a field changed, and created otherwise; rows that are not in the file are left alone, so running the same file again changes nothing. Any error (an unknown key in the document, a bad value, a course key or meeting number that does not exist in the file) names the record and writes nothing. The command prints a table of created, updated, unchanged and deleted rows per kind.
`--prune` makes the file authoritative for what it covers: it deletes the courses of the file's semesters that the file does not list (their sessions and readings go with them, their deadlines and materials are unlinked), then the class sessions and readings of the listed courses that the file does not list. Reading progress of a deleted reading is deleted with it. Meetings and deadlines are never pruned. Each deleted row is printed. Use it when a timetable changes: a moved session is a new start time, so without `--prune` the old one would stay.
`--dry-run` applies the seed (and the prune) in a transaction that is rolled back, so the table and the deleted rows show what a real run would do.
| Kind | Identity |
|------|----------|
@@ -231,4 +236,4 @@ go test ./... # unit tests and Postgres integration tests (Docker)
go test -short ./... # unit tests only
```
The integration tests start a `postgres:16-alpine` container through testcontainers and create a fresh database per test; they never touch a development database. They cover the migrations up and down, the schema constraints, `bm:seed` (idempotency, in-place updates, all-or-nothing), every API route through the real `jwt.auth` guard, and admin saves through the admin API.
The integration tests start a `postgres:16-alpine` container through testcontainers and create a fresh database per test; they never touch a development database. They cover the migrations up and down, the schema constraints, `bm:seed` (idempotency, in-place updates, all-or-nothing, prune and dry run), every API route through the real `jwt.auth` guard, and admin saves through the admin API.