Add plant catalog backend: CRUD + seeded built-ins (#12)
Extends the plants store (which had only the /full read side) with full catalog CRUD, adds a service layer with visibility/immutability rules, REST endpoints, and a seed migration of ~32 built-ins. - 0003_seed_plants.sql: 32 built-in plants (owner_id NULL, read-only), seeded once by the version-tracked migration runner. - store: ListPlantsForActor (built-ins + own), Get/Create/Update/Delete + CountPlantingsForPlant; isForeignKeyViolation backstop for the RESTRICT FK. - service: built-ins are read-only (ErrForbidden); another user's plants are invisible (ErrNotFound); delete of a referenced plant → ErrPlantInUse (409); validation mirrors the schema (category enum, spacing>0, hex color, icon). - api: GET,POST /plants and PATCH,DELETE /plants/:id with the version guard. - Made the migration-count store tests derive their expectation from the files so future migrations don't break them. Service + API tests cover visibility, built-in immutability, validation, version conflict, and delete-in-use. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -38,12 +38,19 @@ func TestMigrateCreatesSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Derive the expected head version from the embedded files so adding a
|
||||
// migration doesn't break this test.
|
||||
migs, err := loadMigrations()
|
||||
if err != nil {
|
||||
t.Fatalf("loadMigrations: %v", err)
|
||||
}
|
||||
wantVersion := migs[len(migs)-1].version
|
||||
var version int
|
||||
if err := db.SQL().QueryRow(`SELECT max(version) FROM schema_migrations`).Scan(&version); err != nil {
|
||||
t.Fatalf("read schema_migrations: %v", err)
|
||||
}
|
||||
if version != 2 {
|
||||
t.Errorf("schema version = %d, want 2", version)
|
||||
if version != wantVersion {
|
||||
t.Errorf("schema version = %d, want %d", version, wantVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,12 +61,16 @@ func TestMigrateIsIdempotent(t *testing.T) {
|
||||
if err := db.Migrate(context.Background()); err != nil {
|
||||
t.Fatalf("second Migrate: %v", err)
|
||||
}
|
||||
migs, err := loadMigrations()
|
||||
if err != nil {
|
||||
t.Fatalf("loadMigrations: %v", err)
|
||||
}
|
||||
var count int
|
||||
if err := db.SQL().QueryRow(`SELECT count(*) FROM schema_migrations`).Scan(&count); err != nil {
|
||||
t.Fatalf("count schema_migrations: %v", err)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Errorf("schema_migrations rows = %d, want 2", count)
|
||||
if count != len(migs) {
|
||||
t.Errorf("schema_migrations rows = %d, want %d", count, len(migs))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user