skills/go-repository/SKILL.md
Repository pattern review for Go codebases using Jet ORM, domain models, and adapter patterns.
npx skillsauth add jcleira/agent-skills go-repositoryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Review Go repositories for correct implementation of the repository pattern with Jet ORM, domain models, and adapter patterns.
Repositories should follow a consistent file organization pattern.
repositories/<resource>/
├── repository.go # Repository struct, constructor, interfaces
├── adapters.go # Domain ↔ Database model conversion
├── create.go # Create operations
├── read.go # Read operations (Get, List, etc.)
├── update.go # Update operations
├── delete.go # Delete operations
└── repository_test.go # Test file with parallel tests
Repositories must use the standard constructor with dependency injection.
New or NewRepositorydbGetter parametertransactor parameterdbGetter and transactor are not stored in struct fields// Issue: missing dependencies
func NewRepository() *Repository {
return &Repository{}
}
// Correct: standard constructor pattern
type Repository struct {
dbGetter dbGetter
transactor transactor
}
func New(dbGetter dbGetter, transactor transactor) *Repository {
return &Repository{
dbGetter: dbGetter,
transactor: transactor,
}
}
// Standard dependency interfaces
type dbGetter interface {
GetDB(ctx context.Context) DBInterface
}
type transactor interface {
WithinTransaction(ctx context.Context, fn func(context.Context) error) error
}
Repository methods must follow consistent signature patterns.
context.Context is not the first parameterGet instead of GetByID)(T, error) for single items (should return (*T, error))// Issue: no context, bad naming, database model return
func (r *Repository) Get(id string) (model.User, error) { ... }
// Correct: context first, clear name, domain model return
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) { ... }
| Method | Signature |
|--------|-----------|
| Get by ID | GetByID(ctx context.Context, id string) (*domain.T, error) |
| List | List(ctx context.Context, filter Filter) ([]*domain.T, error) |
| Create | Create(ctx context.Context, entity *domain.T) (*domain.T, error) |
| Update | Update(ctx context.Context, entity *domain.T) (*domain.T, error) |
| Delete | Delete(ctx context.Context, id string) error |
// Correct: Create with adapter usage
func (r *Repository) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
dbUser := toDBUser(user) // Use adapter
stmt := table.Users.INSERT(
table.Users.AllColumns,
).MODEL(dbUser).RETURNING(table.Users.AllColumns)
var result model.User
if err := stmt.Query(db, &result); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return toDomainUser(&result), nil // Use adapter
}
// Correct: GetByID with qrm.ErrNoRows handling
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
stmt := table.Users.SELECT(table.Users.AllColumns).
WHERE(table.Users.ID.EQ(postgres.String(id)))
var result model.User
if err := stmt.Query(db, &result); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user %s: %w", id, err)
}
return toDomainUser(&result), nil
}
// Correct: List with filter and pagination
func (r *Repository) List(ctx context.Context, filter Filter) ([]*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
stmt := table.Users.SELECT(table.Users.AllColumns)
// Apply filters
if filter.Status != "" {
stmt = stmt.WHERE(table.Users.Status.EQ(postgres.String(filter.Status)))
}
// Apply pagination
stmt = stmt.ORDER_BY(table.Users.CreatedAt.DESC()).
LIMIT(filter.Limit).
OFFSET(filter.Offset)
var results []model.User
if err := stmt.Query(db, &results); err != nil {
return nil, fmt.Errorf("failed to list users: %w", err)
}
return toDomainUsers(results), nil
}
qrm.ErrNoRows// Correct: Update with selective field updates
func (r *Repository) Update(ctx context.Context, user *domain.User) (*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
dbUser := toDBUser(user)
stmt := table.Users.UPDATE(
table.Users.Name,
table.Users.Email,
table.Users.UpdatedAt,
).MODEL(dbUser).
WHERE(table.Users.ID.EQ(postgres.String(user.ID))).
RETURNING(table.Users.AllColumns)
var result model.User
if err := stmt.Query(db, &result); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, fmt.Errorf("failed to update user %s: %w", user.ID, err)
}
return toDomainUser(&result), nil
}
AllColumns (should explicitly list updateable fields)qrm.ErrNoRows// Correct: Delete with existence check
func (r *Repository) Delete(ctx context.Context, id string) error {
db := r.dbGetter.GetDB(ctx)
stmt := table.Users.DELETE().
WHERE(table.Users.ID.EQ(postgres.String(id)))
result, err := stmt.Exec(db)
if err != nil {
return fmt.Errorf("failed to delete user %s: %w", id, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return domain.ErrUserNotFound
}
return nil
}
qrm.ErrNoRows is not checked in read operationsqrm.ErrNoRows returns database error instead of domain error// Issue: no qrm.ErrNoRows check
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
var result model.User
if err := stmt.Query(db, &result); err != nil {
return nil, err // Returns qrm.ErrNoRows directly!
}
return toDomainUser(&result), nil
}
// Correct: convert to domain error
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
var result model.User
if err := stmt.Query(db, &result); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user %s: %w", id, err)
}
return toDomainUser(&result), nil
}
// Domain errors in domain package
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
ErrInvalidUser = errors.New("invalid user")
)
Adapters convert between domain models and database models.
adapters.go)toDomain* and toDB* pattern// Correct: adapter pattern in adapters.go
func toDomainUser(dbUser *model.User) *domain.User {
if dbUser == nil {
return nil
}
return &domain.User{
ID: dbUser.ID,
Email: dbUser.Email,
Name: dbUser.Name,
Status: domain.UserStatus(dbUser.Status),
CreatedAt: dbUser.CreatedAt,
UpdatedAt: dbUser.UpdatedAt,
}
}
func toDBUser(user *domain.User) *model.User {
if user == nil {
return nil
}
return &model.User{
ID: user.ID,
Email: user.Email,
Name: user.Name,
Status: string(user.Status),
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
// Slice adapters
func toDomainUsers(dbUsers []model.User) []*domain.User {
users := make([]*domain.User, len(dbUsers))
for i, dbUser := range dbUsers {
users[i] = toDomainUser(&dbUser)
}
return users
}
| Conversion | Function Name |
|------------|---------------|
| DB → Domain | toDomainUser(*model.User) *domain.User |
| Domain → DB | toDBUser(*domain.User) *model.User |
| DB slice → Domain | toDomainUsers([]model.User) []*domain.User |
Use transactor.WithinTransaction for multi-step operations.
dbGetter.GetDB(ctx) is called outside transaction function// Correct: transaction pattern for multi-step operation
func (r *Repository) CreateUserWithProfile(ctx context.Context, user *domain.User, profile *domain.Profile) error {
return r.transactor.WithinTransaction(ctx, func(txCtx context.Context) error {
db := r.dbGetter.GetDB(txCtx) // Use txCtx, not ctx!
// Step 1: Insert user
dbUser := toDBUser(user)
stmt := table.Users.INSERT(table.Users.AllColumns).
MODEL(dbUser).
RETURNING(table.Users.AllColumns)
var resultUser model.User
if err := stmt.Query(db, &resultUser); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
// Step 2: Insert profile
dbProfile := toDBProfile(profile)
dbProfile.UserID = resultUser.ID
stmt2 := table.Profiles.INSERT(table.Profiles.AllColumns).
MODEL(dbProfile)
if _, err := stmt2.Exec(db); err != nil {
return fmt.Errorf("failed to create profile: %w", err)
}
return nil // Commit on success
})
}
dbGetter.GetDB() is called without contextdb.QueryContext()// Issue: database stored in struct
type Repository struct {
db *sql.DB // Wrong! Use dbGetter
}
// Issue: SQL instead of Jet
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id) // Use Jet!
}
// Correct: Jet ORM usage
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
db := r.dbGetter.GetDB(ctx)
stmt := table.Users.SELECT(table.Users.AllColumns).
WHERE(table.Users.ID.EQ(postgres.String(id)))
var result model.User
if err := stmt.Query(db, &result); err != nil {
if errors.Is(err, qrm.ErrNoRows) {
return nil, domain.ErrUserNotFound
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return toDomainUser(&result), nil
}
db.QueryContext(ctx, ...) with contextRepository tests should be thorough and follow Go testing conventions.
t.Parallel()// Correct: table-driven parallel tests
func TestRepository_GetByID(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
want *domain.User
wantErr error
}{
{
name: "existing user",
id: "user-123",
want: &domain.User{ID: "user-123", Name: "John"},
wantErr: nil,
},
{
name: "non-existent user",
id: "user-999",
want: nil,
wantErr: domain.ErrUserNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Setup
repo := setupTestRepo(t)
if tt.want != nil {
seedUser(t, repo, tt.want)
}
// Execute
got, err := repo.GetByID(context.Background(), tt.id)
// Assert
if !errors.Is(err, tt.wantErr) {
t.Errorf("GetByID() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetByID() = %v, want %v", got, tt.want)
}
})
}
}
model.*)// Issue: leaking database model
func (r *Repository) GetByID(ctx context.Context, id string) (*model.User, error) {
// Returns database model!
}
// Correct: return domain model
func (r *Repository) GetByID(ctx context.Context, id string) (*domain.User, error) {
var result model.User
if err := stmt.Query(db, &result); err != nil {
// handle error
}
return toDomainUser(&result), nil // Convert before return
}
// Issue: repository doing too much
func (r *Repository) GetUserWithOrdersAndPayments(ctx context.Context, id string) (*ComplexDTO, error) {
// Multiple joins, complex business logic
}
Solution: Keep repositories focused on single aggregate root.
// Issue: validation in repository
func (r *Repository) Create(ctx context.Context, user *domain.User) error {
if user.Email == "" {
return errors.New("email required") // Wrong layer!
}
}
Solution: Validate in service layer, not repository.
// Issue: not checking ErrNoRows
if err := stmt.Query(db, &result); err != nil {
return nil, err // Exposes database error!
}
Solution: Always check qrm.ErrNoRows and convert to domain error.
// Issue: storing connection
type Repository struct {
db *sql.DB
}
Solution: Use dbGetter.GetDB(ctx) to get connection per operation.
When reviewing repository code, report findings as:
**Repository Pattern Review**
Issues found:
- `repositories/user/repository.go:15` - Constructor missing `transactor` parameter
- `repositories/user/create.go:42` - Returns `*model.User` instead of `*domain.User`
- `repositories/user/read.go:58` - GetByID doesn't check for `qrm.ErrNoRows`
- `repositories/user/adapters.go:12` - Adapter `ToDomainUser` is public (should be private)
Recommendations:
- Add `repository_test.go` with table-driven tests
- Separate CRUD methods into individual files
- Add domain error definitions for NotFound cases
No issues:
- Constructor follows standard pattern
- Adapters properly convert domain ↔ database models
- Transaction pattern used correctly in CreateWithProfile
This skill auto-invokes when reviewing Go files that:
repositories/ or repository/ directoriesdbGetter or transactor fieldsgithub.com/go-jet/jet)development
Go style and idioms from official Go Code Review Comments wiki.
development
DDD architecture review for Go projects enforcing layer boundaries, pointer semantics, and domain-driven design patterns.
development
Review Go code using Gin framework for routing patterns, middleware usage, request/response handling, and error conventions.
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.