Skip to content

pkg/seed

Seed data for CoreAPI's own coreapi database (roles, orgs, layouts, and domain starter data like media transcoding profiles). Runs unconditionally on every server boot via seed.Run(db.GetDB()), called from main.go right after db.RunMigrationMain().

Not related to the ticketing (johorzoo) database's SQL dump files โ€” see Migrations.


Entry point

pkg/seed/00-seed.go โ€” Run(db) calls each domain's seedX(db) function in a fixed order (media first, then platform/roles/apps, then workspace/layout/playlist data, then devices/teams/channels). Order matters where one seed's rows reference another's โ€” e.g. media profiles before anything that might reference a default profile ID.


Two seeding patterns in this package โ€” know which one a file uses

Most seed files (apps.go, roles.go, tags.go, workspaces.go, teams.go, venue.go, locations.go, endpoints.go, layout.go, playlists.go, mediachannel.go, devices.go, organization.go) use the upsert() helper (00-seed.go) or an explicit Firstโ†’ continue-if-found check (transcoding_presets.go, the Playback domain's own preset seed):

func upsert(db *gorm.DB, table string, value interface{}, conflictCols ...string) {
    db.Clauses(clause.OnConflict{Columns: cols, DoNothing: true}).Create(value)
}

This is insert-if-not-exists, not a real upsert โ€” the name is historical. If you change a seed row's values in code and redeploy, an existing row with the same conflict key is left exactly as it was first created. This is fine for genuinely one-time bootstrap data (a platform org, a default role), but it silently does nothing for anything you expect to keep tuning during development.

mediatranscoding_presets.go uses a different, newer pattern โ€” converge by Code:

func seedMediaProfile(db *gorm.DB, s mediaProfileSeed) {
    // found by Code โ†’ map-based Updates() converges name/category/isDefault
    // and the CURRENT version's renditions/settings/segmentSecs to match
    // this file, in place โ€” no new version row inserted on reseed.
    // not found โ†’ creates profile + version 1.
}

Every boot converges the DB row to exactly what the Go file currently says. Use this pattern (not upsert()) for anything you expect to keep editing as defaults shift during development โ€” see Transcoding ยง Seeded Profiles for the actual current values.


Pruning rows removed from the seed file

Converging by Code only handles a Code that's still present in the file. Deleting or renaming an entry doesn't remove its old row โ€” nothing before this was tracking "codes no longer defined here at all."

seedMediaTranscodingProfiles now tracks every Code it seeds in a given run and calls pruneObsoleteMediaProfiles(db, seededCodes) at the end:

// Deletes (hard, Unscoped) any TranscodingProfile with a non-null Code
// not in the current run's seeded list. Non-null Code is the signal โ€”
// CRUD-created profiles never set one, so this never touches anything
// created through the UI.
func pruneObsoleteMediaProfiles(db *gorm.DB, currentCodes []string) { ... }

Hard-deletes rather than soft-deletes deliberately โ€” Code has a unique index, and a soft-deleted row would still occupy that value, so reintroducing the same Code later would fail the unique constraint instead of reseeding cleanly.

This runs on every boot, unconditionally, in every environment

Unlike the hard-reset tool below, pruning is not gated behind any flag. Today that's safe because every seeded profile is still a "starter default." But editing a seeded profile for real through the CRUD UI (UpdateTranscodingProfile) does not clear its Code โ€” so a profile someone is actively relying on can still be tagged with its original seed Code. If that Code is later renamed or removed from the seed file for an unrelated reason, the live profile is silently hard-deleted on the next deploy. Treat a Code as a permanent identity once anything real depends on the row it points at โ€” don't repurpose or remove one casually once this reaches an environment where that matters. If this needs to run in production one day, gate pruning behind the same flag as the hard-reset tool below rather than leaving it unconditional.


Companion tool: hard-resetting a domain's tables entirely

For schema churn heavy enough that converge-by-Code isn't enough โ€” you're changing column shapes, not just seed values โ€” pkg/db has HardResetTables(tables ...string): drops the given tables (CASCADE) so the next AutoMigrate recreates them fresh from the current struct shape, instead of trying (and sometimes failing) to ALTER an existing one into it. See Migrations ยง Hard-resetting a domain for the mechanics and safety notes, and domain/media's guide for the one live example (MEDIA_HARD_RESET=true, media domain only).

This wipes all data in the listed tables โ€” only ever enable it in a disposable environment (staging, pre-launch), gated behind an env var that defaults off.