add seeder
Reach for a seeder whenever the project needs baseline data to exist after migrations run, but before anyone can use the app: an admin account to log in with, a fixed set of roles or lookup values a foreign key depends on, demo data for a staging environment. Business rows created in response to user action belong in a service, not here; a seeder is for rows the app assumes are already there.
go tool gpsystem add seeder <name>
Writes a seeder stub into the project's central seeds/ package and registers it in seeds/register.go. The seeder returns a seed.Seeder: a name, an optional Guard (decides whether it runs at all), and the Run body. Details and patterns: Seeders.
go tool gpsystem add seeder adminUser
What it generates
func adminUser(deps Deps) seed.Seeder {
return seed.Seeder{
Name: "admin_user",
Run: func(ctx context.Context) error {
return nil
},
}
}
func Register(reg *seed.Registry, deps Deps) {
reg.Add(adminUser(deps))
// gpsystem:seeds
}
Deps (in seeds/register.go, present in every project since new project) carries a *pg.DB or *bun.DB field per the project's --db choice, plus a dbx.Transactor. The command expects the seeds/register.go every project has had since new project; if it's missing, it fails and says so.
A worked, multi-row example
Seeders run in registration order (Add preserves it), which matters the moment one seeder's rows are a foreign key for another's: a set of roles has to exist before the admin user that references one. Two seeders, added in that order:
go tool gpsystem add seeder roles
go tool gpsystem add seeder adminUser
func roles(deps Deps) seed.Seeder {
names := []string{"admin", "editor", "viewer"}
return seed.Seeder{
Name: "roles",
Run: func(ctx context.Context) error {
for _, name := range names {
if _, err := deps.DB.NewInsert().
Model(&Role{Name: name}).
On("CONFLICT (name) DO NOTHING").
Exec(ctx); err != nil {
return fmt.Errorf("seed role %q: %w", name, err)
}
}
return nil
},
}
}
func adminUser(deps Deps) seed.Seeder {
return seed.Seeder{
Name: "admin_user",
Guard: func(ctx context.Context) (bool, error) {
exists, err := deps.DB.NewSelect().Model((*User)(nil)).
Where("email = ?", "admin@example.com").Exists(ctx)
return !exists, err
},
Run: func(ctx context.Context) error {
hash, err := bcrypt.GenerateFromPassword([]byte(mustEnv("SEED_ADMIN_PASSWORD")), bcrypt.DefaultCost)
if err != nil {
return err
}
_, err = deps.DB.NewInsert().Model(&User{
Email: "admin@example.com",
PasswordHash: string(hash),
Role: "admin",
}).Exec(ctx)
return err
},
}
}
func Register(reg *seed.Registry, deps Deps) {
reg.Add(roles(deps))
reg.Add(adminUser(deps))
// gpsystem:seeds
}
roles uses ON CONFLICT DO NOTHING to stay idempotent across reruns on its own; adminUser's Guard does the same job by checking for the row first, so the run is logged as a clean skip instead of a duplicate-key error on the second migrate up --seed. Both are valid; pick whichever reads more naturally for the data at hand.
Running it
go run ./cmd/migrate up --seed # migrate, then seed
go run ./cmd/migrate seed # seed only
add seeder only writes and registers the stub: the business logic (what to load, under what condition) is yours to write into the generated file. See Seeders for the Guard, ErrSkip and multi-tenant WithTenants patterns.