Factories#
// vukem make:factory User
var User = factory.New(func(f *factory.Faker) models.User {
return models.User{Name: f.Name(), Email: f.Email()}
})admins, err := factories.User.Count(3).State(func(u *models.User) { u.Role = "admin" }).Create(ctx, db)
draft := factories.User.MakeOne() // not saved
same := factories.User.Seed(42).Count(5).Make() // reproducible data
// Related models: AfterCreate runs inside Create's transaction.
withPosts := factories.User.AfterCreate(func(ctx context.Context, ex database.Executor, u *models.User) error {
_, err := factories.Post.Count(2).State(func(p *models.Post) { p.UserID = u.ID }).Create(ctx, ex)
return err
})- Factories are values:
State,Count,SeedandAfterCreatereturn copies, so a base factory can be shared. factory.Fakerhas no dependency:Name,FirstName,LastName,Username,Email(reserved example domains),Word,Words,Sentence,Paragraph,Slug,URL,UUID,Int,Float,Bool,Time,factory.Pick(f, ...)andSeq(). It usesmath/rand/v2and must never generate secrets.
Seeders#
// vukem make:seeder Posts (registered in database/seeders/registry.go)
type PostsSeeder struct{}
func (PostsSeeder) Seed(ctx context.Context, db *database.DB) error {
_, err := factories.Post.Count(10).Create(ctx, db)
return err
}vukem db:seed # runs every seeder in vukem.WithSeeders(seeders.All()...), in order
vukem db:wipe # drops every tableBoth commands need --force in production. Tests call app.Seed(ctx).
Seeders that need the app#
A Seeder only gets the database. A seeder that reads configuration, logs, or uses a service a provider opened (an S3 disk for seeded images) implements vukem.AppSeeder instead and is registered with vukem.WithAppSeeders:
func Users() vukem.AppSeeder {
return vukem.NamedAppSeeder("Users", func(ctx context.Context, app *vukem.App) error {
if app.Config.IsProduction() {
return errors.New("demo users are not seeded in production")
}
db, err := app.DB()
if err != nil {
return err
}
password := app.Source.Get("SEED_PASSWORD")
app.Log.InfoContext(ctx, "seeding demo users")
return createDemoUsers(ctx, db, password)
})
}vukem.WithSeeders(seeders.All()...),
vukem.WithAppSeeders(seeders.Users()),Both kinds run in one list, in option order. db:seed prints Seeded: <name> and errors read seed <name>: ..., where the name is the seeder's Name() string method when it has one, else its Go type (AppSeederFunc for every function); vukem.NamedAppSeeder("Users", fn) gives a function a name. db:seed boots the app before seeding, so services providers open in Boot are ready; call app.Boot(ctx) before app.Seed(ctx) when you seed from your own code. A provider's service can be handed to the seeder when bootstrap builds both (seeders.Media(mediaProvider)).
Database validation rules#
z schemas check shape and format without I/O. Rules that need the database run after parsing and return the same 422 validation error (flashed for forms):
in, err := web.Parse(c, requests.RegisterSchema)
if err != nil {
return err
}
if err := orm.Validate(c.Context(), db,
orm.Unique[models.User]("email", in.Email), // soft-deleted rows count, like the index
orm.Unique[models.User]("username", in.Username).Ignore(current.ID), // updates keep their own value
orm.Exists[models.Team]("id", in.TeamID).Field("team_id").Message("Pick a valid team."),
); err != nil {
return err
}The built-in messages follow the request locale (validation.unique, validation.exists, with {field} from validation.attributes.<column>; see Localization); a Message override is used as is.
Empty values are skipped (required belongs to the schema). Columns are validated against the model.