Repositories
A repository
Section titled “A repository”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}Using it from a controller
Section titled “Using it from a controller”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.