Skip to content

Repositories

The sql module gives a ready-made one: sql.ForFeature[User]("users") provides *sql.Repository[User] with Find, List, Page, Create, Update, Patch and Delete, built from the struct’s db tags (see sql/doc.go). The hand written version below shows the same idea without the module, for pgx pools, GORM or ent.

Define the interface where it is used, and bind the Postgres implementation to it with dygo.As. Map sql.ErrNoRows to your own error, so the controller does not depend on database/sql.

type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
}
// Repo is what the service needs. Tests replace it with a fake.
type Repo interface {
Find(ctx context.Context, id int64) (User, error)
Create(ctx context.Context, email string) (User, error)
}
var ErrNotFound = errors.New("user not found")
type PgRepo struct{ db *database.DB }
func NewPgRepo(db *database.DB) *PgRepo { return &PgRepo{db: db} }
func (r *PgRepo) Find(ctx context.Context, id int64) (User, error) {
var u User
err := r.db.QueryRowContext(ctx, `SELECT id, email FROM users WHERE id = $1`, id).Scan(&u.ID, &u.Email)
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNotFound
}
return u, err
}
func (r *PgRepo) Create(ctx context.Context, email string) (User, error) {
u := User{Email: email}
err := r.db.InTx(ctx, func(tx *sql.Tx) error {
if err := tx.QueryRowContext(ctx, `INSERT INTO users (email) VALUES ($1) RETURNING id`, email).Scan(&u.ID); err != nil {
return err
}
_, err := tx.ExecContext(ctx, `INSERT INTO audit (user_id, action) VALUES ($1, 'created')`, u.ID)
return err
})
return u, err
}
func (c *Controller) Get(ctx context.Context, in GetIn) (User, error) {
u, err := c.repo.Find(ctx, in.ID)
if errors.Is(err, ErrNotFound) {
return User{}, errs.NotFound("user not found")
}
return u, err // any other error is a logged 500
}
var Module = &dygo.Module{
Name: "users",
Providers: []dygo.Provider{dygo.Provide(NewPgRepo, dygo.As[Repo]())},
Controllers: []dygo.Provider{dygo.Provide(NewController)},
}

Errors that are not *errs.HTTPError become a 500 with a generic body, and the real error (with the SQL detail) is logged, never sent to the client.