Files
summercms/.planning/phases/05-data-layer-full-fidelity/05-02-SUMMARY.md
Jakub Zych 628cffd66f docs(05-02): complete album-collection vertical slice plan
- Record migrations, Jsonable/MoneyString, Validate, write services, and hooks
2026-09-18 19:23:51 +02:00

10 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
phase plan subsystem tags requires provides affects tech-stack key-files key-decisions patterns-established requirements-completed duration completed
05-data-layer-full-fidelity 02 database
gorm
gormigrate
jsonable
money
validation
pivot
lifecycle
fillable
phase provides
05-data-layer-full-fidelity Winter-directory registries, lagoon.Fill, WithSoftDeleteCascade, RegisterJoinTable
six fonoteka album-slice migrations with idempotent Various Artists seed
widened Album/Collection plus Artist, Style, AlbumArtist, AlbumRating, AlbumReservation
lagoon.Jsonable[T] TEXT cast preserving SQL NULL vs empty array
MoneyString 4-decimal numeric cast with no binary floating type
lagoon.Validate Laravel rule-string engine with unique:table soft-delete scoping
Artist/Genre/Style/Collection lifecycle hooks and Album.BeforeSave
AlbumWriteService/CollectionWriteService fill boundaries and syncArtists
05-03-secrets
05-04-attachments
05-05-remaining-models
05-06-tests
added patterns
github.com/go-playground/validator/v10 v10.30.4
Jsonable struct with gorm-ignored inner fields so GORM uses Scanner/Valuer
pivot writes via delete-then-bulk-insert, never Association().Append
KeepMarketPriceSource gorm:"-" flag threads source provenance into BeforeSave
created modified
lagoon/jsonable.go
lagoon/jsonable_test.go
lagoon/validate.go
lagoon/validate_test.go
plugins/golem15/fonoteka/updates/10_album_slice.go
plugins/golem15/fonoteka/models/artist.go
plugins/golem15/fonoteka/models/style.go
plugins/golem15/fonoteka/models/album_artist.go
plugins/golem15/fonoteka/models/album_rating.go
plugins/golem15/fonoteka/models/album_reservation.go
plugins/golem15/fonoteka/models/money_string.go
plugins/golem15/fonoteka/models/slug.go
plugins/golem15/fonoteka/classes/album_write_service.go
plugins/golem15/fonoteka/classes/collection_write_service.go
plugins/golem15/fonoteka/classes/artist_resolver.go
plugins/golem15/fonoteka/classes/join_tables.go
plugins/golem15/fonoteka/classes/serialize.go
lagoon/fill.go
plugins/golem15/fonoteka/models/album.go
plugins/golem15/fonoteka/models/collection.go
plugins/golem15/fonoteka/models/collection_editor.go
plugins/golem15/fonoteka/models/genre.go
plugins/golem15/fonoteka/go.mod
parity/migrate_test.go
Jsonable payload field is Data, not Value, because driver.Valuer.Value() collides under go vet
KeepMarketPriceSource is a gorm-ignored Album flag set by SaveAlbum when requested contains market_price_source
CollectionFillFields is name+description; PHP has no CollectionWriteService
Various Artists natural key is name_key='various artists' (PHP seed), slug various-artists
go-playground/validator v10.30.4 is the STACK-named rule engine behind lagoon.Validate
One Register call per model/migration/class file init(); plugin.go is not edited
Pivot business columns are written with explicit SQL/GORM Create, Preload uses RegisterJoinTable
Money never lives as a binary floating type; range checks use big.Rat
DATA-03
DATA-04
DATA-05
DATA-06
DATA-07
DATA-09
27min 2026-09-18

Phase 5 Plan 02: Album/Collection vertical slice Summary

Album/Collection slice with six gormigrate steps, Jsonable/MoneyString casts, lagoon.Validate, ordered album_artists pivot writes, and PHP lifecycle hooks proven on real Postgres

Performance

  • Duration: 27 min
  • Started: 2026-09-18T16:55:11Z
  • Completed: 2026-09-18T17:22:30Z
  • Tasks: 3
  • Files modified: 32

Accomplishments

  • Six Fonoteka migrations widen collections/albums (including track_titles and TEXT discogs_id) and create artists (Various Artists seed), styles, ratings, and reservations; each rolls back in isolation
  • lagoon.Jsonable[T] round-trips SQL NULL vs '[]'; MoneyString formats Postgres numeric(10,4) to a 4-decimal string with no binary floating type in the cast
  • Artist/Genre/Style BeforeValidate/AfterDelete and Collection BeforeDelete cascade (via WithSoftDeleteCascade) are ported from PHP and tested against real Postgres
  • lagoon.Validate translates this slice's rule strings (between, quoted oneof, custom money range, unique:table with deleted_at) and fails loudly on unknown tokens
  • SaveAlbum uses the PHP FILL_FIELDS subset, syncArtists delete-then-insert, and Album.BeforeSave (refreshTrackTitles + stampMarketPrice) including the PHP ceiling case "12.55555" → "12.5556"

Task Commits

Each task was committed atomically:

  1. Task 1: Album/collection slice migrations - e24a614 (feat) in fonoteka.go
  2. Task 2: lagoon.Jsonable - 20b0199 (feat) in summercms.go
  3. Task 2: Widen models and port lifecycle hooks - 445133e (feat) in fonoteka.go
  4. Task 3: lagoon.Validate - 2c5d92d (feat) in summercms.go
  5. Task 3: Write services and BeforeSave - 4879996 (feat) in fonoteka.go

Plan metadata: pending (this file)

Files Created/Modified

  • lagoon/jsonable.go, lagoon/jsonable_test.go — TEXT JSON cast, NULL vs []
  • lagoon/validate.go, lagoon/validate_test.go — rule-string engine
  • lagoon/fill.go — nil zeros the field; jsonable columns Scan from JSON
  • fonoteka.go/plugins/golem15/fonoteka/updates/10_album_slice.go — six migrations
  • fonoteka.go/plugins/golem15/fonoteka/models/album.go — final columns, Fillable/Rules, BeforeSave
  • fonoteka.go/plugins/golem15/fonoteka/models/{artist,style,album_artist,album_rating,album_reservation,money_string,slug}.go
  • fonoteka.go/plugins/golem15/fonoteka/models/{collection,collection_editor,genre}.go — widened + hooks
  • fonoteka.go/plugins/golem15/fonoteka/classes/{album_write_service,collection_write_service,artist_resolver,join_tables,serialize}.go
  • fonoteka.go/parity/migrate_test.go, album_slice_models_test.go, album_write_service_test.go

Decisions Made

  • Jsonable payload is Data (not Value) because driver.Valuer.Value() plus a field named Value fails go vet
  • KeepMarketPriceSource is a gorm:"-" flag on Album, set by SaveAlbum when requested contains market_price_source
  • CollectionFillFields is name, description — PHP has no CollectionWriteService, so the service list is a subset of Fillable() excluding owner_id
  • Various Artists seed uses PHP's natural key name_key = "various artists" (slug various-artists)
  • github.com/go-playground/validator/v10 v10.30.4 is added to the framework module as the STACK-named engine behind lagoon.Validate

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Jsonable cannot use a field named Value

  • Found during: Task 2 (Jsonable cast)
  • Issue: go vet rejects a struct field and method both named Value; type Jsonable[T] T is also illegal in Go
  • Fix: Struct with Data/Valid/NullOnEmpty tagged gorm:"-" so GORM uses Scanner/Valuer instead of walking inner fields
  • Files modified: lagoon/jsonable.go, lagoon/jsonable_test.go
  • Verification: go vet ./lagoon/... and go test ./lagoon/... -run TestJsonable
  • Committed in: 20b0199 (Task 2)

2. [Rule 2 - Missing Critical] Fill ignored nil on non-pointer fields

  • Found during: Task 3 (SaveAlbum money nil-in → NULL-out)
  • Issue: lagoon.Fill left MoneyString unchanged on explicit nil, so the blank-to-NULL path could not be requested
  • Fix: nil now zeros any field; types implementing sql.Scanner (Jsonable) Scan from JSON when convertValue cannot assign
  • Files modified: lagoon/fill.go
  • Verification: TestSaveAlbumMoneyNormalization nil case
  • Committed in: 2c5d92d (Task 3)

3. [Rule 3 - Blocking] Existing migrate tests hardcoded two Fonoteka IDs

  • Found during: Task 1 (six new migrations)
  • Issue: TestMigrateSeedsCanonicalGenres and TestRollbackLastIsolatesFonoteka expected exactly the Phase-3 pair
  • Fix: Expect eight IDs; RollbackLast now asserts create_album_reservations drops only that table
  • Files modified: fonoteka.go/parity/migrate_test.go
  • Verification: those tests plus TestAlbumSliceMigrationsUpDown
  • Committed in: e24a614 (Task 1)

4. [Rule 2 - Missing Critical] widen_albums/collections needed columns omitted from Phase 3 stubs

  • Found during: Task 1 (reading folded PHP files)
  • Issue: description (collections), public_enabled/public_token_generated_at, and shelf/quantity/notes exist in PHP but not in 00_base.go
  • Fix: Added every column the folded PHP files (and the RESEARCH fillable list) require
  • Files modified: fonoteka.go/plugins/golem15/fonoteka/updates/10_album_slice.go
  • Verification: TestAlbumSliceMigrationsUpDown and later model tests
  • Committed in: e24a614 (Task 1)

Total deviations: 4 auto-fixed (2 missing critical, 2 blocking) Impact on plan: Required for vet, money nil-out, and a compiling schema that matches PHP. No scope creep. Album.Fillable() has 21 names copied from PHP/RESEARCH (plan said 20 — a count error).

Issues Encountered

None beyond the auto-fixes above.

Authentication Gates

None.

Known Stubs

  • classes/artist_resolver.go albumBeforeSaveCallback is a type-guard registration seam; name-to-id find-or-create stays in PHP's ArtistResolver::resolveOne and is not needed for this plan's ID-based syncArtists
  • SerializeAlbum/SerializeCollection omit photos/thumbs (Plan 05-04) and reservation context (Phase 12)

User Setup Required

None - no external service configuration required.

Next Phase Readiness

Ready for 05-03 (secrets/encrypted credentials). Wave-2 Album/Collection models, validations, and write-service fill boundaries are in place. lagoon.Validate and lagoon.Jsonable are reusable for later models.

Self-Check: PASSED

  • Key files exist on disk (jsonable, validate, 10_album_slice, write services, join_tables, models)
  • Commits e24a614, 20b0199, 445133e, 2c5d92d, 4879996 exist
  • go vet ./... and go test ./lagoon/... green in summercms.go
  • go vet ./... and go test ./... green in fonoteka.go (parity + plugin modules)

Phase: 05-data-layer-full-fidelity Completed: 2026-09-18