crud

package module
v1.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 3, 2026 License: MIT Imports: 13 Imported by: 0

README

go-crud

Go Version Test Coverage License

A generic CRUD repository library for Go applications using GORM, providing type-safe database operations with transaction support, query scopes, and comprehensive security features.

🚀 Features

  • 🔒 Type-Safe Operations: Generic interfaces for compile-time type safety
  • 📊 Transaction Management: Context-aware transactions with nested support
  • 🔍 Query Scopes: Reusable query builders for pagination, filtering, and ordering
  • 🛡️ Security First: SQL injection prevention and field name validation
  • ⚡ Performance: Optimized queries with preloading and batch operations
  • 🧪 Well Tested: 84.3% test coverage with comprehensive test suite
  • 📖 Clean API: Intuitive interfaces following Go best practices

📦 Installation

go get github.com/itsLeonB/go-crud

🏗️ Architecture

The library is built around three core components:

1. CRUDRepository - Type-safe database operations
2. Transactor - Transaction management with context
3. Query Scopes - Reusable query builders

🔧 Quick Start

Basic Setup
package main

import (
    "context"
    "log"

    "github.com/itsLeonB/go-crud"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

// Define your model
type User struct {
    ID        uint      `gorm:"primaryKey"`
    Name      string    `gorm:"not null"`
    Email     string    `gorm:"unique"`
    Age       int
    CreatedAt time.Time
    UpdatedAt time.Time
}

func main() {
    // Initialize GORM
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal("Failed to connect to database:", err)
    }

    // Auto-migrate your models
    db.AutoMigrate(&User{})

    // Create repository and transactor
    userRepo := crud.NewCRUDRepository[User](db)
    transactor := crud.NewTransactor(db)

    ctx := context.Background()

    // Your application logic here...
}

📚 Core Components

CRUDRepository Interface

The CRUDRepository provides type-safe CRUD operations:

type CRUDRepository[T any] interface {
    Insert(ctx context.Context, model T) (T, error)
    FindAll(ctx context.Context, spec Specification[T]) ([]T, error)
    FindFirst(ctx context.Context, spec Specification[T]) (T, error)
    Update(ctx context.Context, model T) (T, error)
    Delete(ctx context.Context, model T) error
    BatchInsert(ctx context.Context, models []T) ([]T, error)
    GetGormInstance(ctx context.Context) (*gorm.DB, error)
}
Specification Pattern

Use specifications to build dynamic queries:

type Specification[T any] struct {
    Model            T        // Model with fields set for WHERE conditions
    PreloadRelations []string // Relations to eager load
    ForUpdate        bool     // Whether to use SELECT ... FOR UPDATE
}

💡 Usage Examples

Basic CRUD Operations
ctx := context.Background()
userRepo := crud.NewCRUDRepository[User](db)

// Create a user
user := User{
    Name:  "John Doe",
    Email: "john@example.com",
    Age:   30,
}
createdUser, err := userRepo.Insert(ctx, user)
if err != nil {
    log.Fatal("Failed to create user:", err)
}

// Find users
spec := crud.Specification[User]{
    Model: User{Age: 30}, // Find users with age 30
}
users, err := userRepo.FindAll(ctx, spec)
if err != nil {
    log.Fatal("Failed to find users:", err)
}

// Find first user
firstUser, err := userRepo.FindFirst(ctx, spec)
if err != nil {
    log.Fatal("Failed to find user:", err)
}

// Update user
createdUser.Name = "John Smith"
updatedUser, err := userRepo.Update(ctx, createdUser)
if err != nil {
    log.Fatal("Failed to update user:", err)
}

// Delete user
err = userRepo.Delete(ctx, updatedUser)
if err != nil {
    log.Fatal("Failed to delete user:", err)
}
Batch Operations
// Batch insert multiple users
users := []User{
    {Name: "Alice", Email: "alice@example.com", Age: 25},
    {Name: "Bob", Email: "bob@example.com", Age: 30},
    {Name: "Charlie", Email: "charlie@example.com", Age: 35},
}

createdUsers, err := userRepo.BatchInsert(ctx, users)
if err != nil {
    log.Fatal("Failed to batch insert users:", err)
}
Advanced Queries with Specifications
// Find users with preloaded relations
spec := crud.Specification[User]{
    Model:            User{Age: 25},
    PreloadRelations: []string{"Profile", "Posts"},
    ForUpdate:        false,
}
users, err := userRepo.FindAll(ctx, spec)

// Pessimistic locking
spec = crud.Specification[User]{
    Model:     User{ID: 1},
    ForUpdate: true, // SELECT ... FOR UPDATE
}
user, err := userRepo.FindFirst(ctx, spec)

🔄 Transaction Management

Basic Transactions
transactor := crud.NewTransactor(db)

err := transactor.WithinTransaction(ctx, func(txCtx context.Context) error {
    // All operations within this function use the same transaction

    user := User{Name: "Alice", Email: "alice@example.com"}
    createdUser, err := userRepo.Insert(txCtx, user)
    if err != nil {
        return err // Transaction will be rolled back
    }

    // Update in same transaction
    createdUser.Age = 25
    _, err = userRepo.Update(txCtx, createdUser)
    if err != nil {
        return err // Transaction will be rolled back
    }

    return nil // Transaction will be committed
})

if err != nil {
    log.Fatal("Transaction failed:", err)
}
Manual Transaction Control
// Begin transaction
txCtx, err := transactor.Begin(ctx)
if err != nil {
    log.Fatal("Failed to begin transaction:", err)
}

// Perform operations
user, err := userRepo.Insert(txCtx, User{Name: "Bob"})
if err != nil {
    transactor.Rollback(txCtx)
    log.Fatal("Failed to insert user:", err)
}

// Commit transaction
err = transactor.Commit(txCtx)
if err != nil {
    log.Fatal("Failed to commit transaction:", err)
}
Nested Transactions
err := transactor.WithinTransaction(ctx, func(outerTxCtx context.Context) error {
    // Outer transaction
    user, err := userRepo.Insert(outerTxCtx, User{Name: "Outer"})
    if err != nil {
        return err
    }

    // Nested transaction (reuses the same transaction)
    return transactor.WithinTransaction(outerTxCtx, func(innerTxCtx context.Context) error {
        // Inner operations use the same transaction
        return userRepo.Update(innerTxCtx, user)
    })
})

🔍 Query Scopes

The library provides powerful query scopes for common operations:

Pagination
db.Scopes(crud.Paginate(page, limit)).Find(&users)

// Example: Get page 2 with 10 items per page
db.Scopes(crud.Paginate(2, 10)).Find(&users)
Ordering
// Order by name ascending
db.Scopes(crud.OrderBy("name", true)).Find(&users)

// Order by created_at descending
db.Scopes(crud.OrderBy("created_at", false)).Find(&users)

// Default ordering (created_at DESC)
db.Scopes(crud.DefaultOrder()).Find(&users)
Filtering
// Filter by specification
spec := User{Age: 25, Name: "Alice"}
db.Scopes(crud.WhereBySpec(spec)).Find(&users)

// Time range filtering
start := time.Now().Add(-24 * time.Hour)
end := time.Now()
db.Scopes(crud.BetweenTime("created_at", start, end)).Find(&users)
Preloading Relations
relations := []string{"Profile", "Posts", "Comments"}
db.Scopes(crud.PreloadRelations(relations)).Find(&users)
Pessimistic Locking
// Add FOR UPDATE clause
db.Scopes(crud.ForUpdate(true)).First(&user, id)
Combining Scopes
// Complex query with multiple scopes
db.Scopes(
    crud.WhereBySpec(User{Age: 25}),
    crud.OrderBy("name", true),
    crud.Paginate(1, 10),
    crud.PreloadRelations([]string{"Profile"}),
).Find(&users)

🛡️ Security Features

SQL Injection Prevention

The library includes robust field name validation to prevent SQL injection:

// Safe field names (allowed)
"name"           ✅
"created_at"     ✅
"users.name"     ✅
"table.column"   ✅

// Dangerous field names (rejected)
"name'; DROP TABLE users; --"  ❌
"name' OR '1'='1"              ❌
".invalid"                     ❌
"invalid."                     ❌
"table..column"                ❌
Field Name Validation Rules
  • Only ASCII letters, digits, underscores, and dots allowed
  • No leading or trailing dots
  • No consecutive dots
  • No empty strings
  • No special characters or SQL keywords

🧪 Testing

The library comes with comprehensive tests achieving 84.3% coverage:

# Run all tests
go test -v ./test/...

# Run tests with coverage
go test -v -coverprofile=coverage.out -coverpkg=./... ./test/...

# Generate coverage report
go tool cover -html=coverage.out -o coverage.html

# Use the test runner script
./test/run_tests.sh
Test Categories
  • Unit Tests: All public functions and methods
  • Integration Tests: Database operations with SQLite
  • Security Tests: SQL injection prevention
  • Concurrency Tests: Thread safety validation
  • Performance Tests: Benchmarks for critical functions

📁 Project Structure

go-crud/
├── gorm_crud_repository.go    # Main CRUD repository implementation
├── gorm_scopes.go            # Query scopes and builders
├── gorm_transactor.go        # Transaction management
├── internal/
│   ├── gorm.go              # Internal GORM utilities
│   └── transactor.go        # Internal transaction logic
├── lib/
│   └── constants.go         # Library constants
└── test/                    # Comprehensive test suite
    ├── crud_repository_test.go
    ├── scopes_test.go
    ├── transactor_test.go
    ├── internal_test.go
    ├── constants_test.go
    └── run_tests.sh

🔧 Configuration

Database Drivers

The library works with any GORM-supported database:

// PostgreSQL
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})

// MySQL
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

// SQLite
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
GORM Configuration
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
    Logger: logger.Default.LogMode(logger.Info),
    NamingStrategy: schema.NamingStrategy{
        TablePrefix:   "app_",
        SingularTable: false,
    },
})

🚀 Performance Tips

1. Use Batch Operations
// Instead of multiple Insert calls
users := []User{{Name: "Alice"}, {Name: "Bob"}}
userRepo.BatchInsert(ctx, users)
2. Preload Relations Wisely
// Only preload what you need
spec := crud.Specification[User]{
    PreloadRelations: []string{"Profile"}, // Not all relations
}
3. Use Pagination
// For large datasets
db.Scopes(crud.Paginate(page, 50)).Find(&users)
4. Leverage Transactions
// Group related operations
transactor.WithinTransaction(ctx, func(txCtx context.Context) error {
    // Multiple related operations
    return nil
})

🤝 Contributing

Contributions are welcome! Please ensure:

  1. Tests: Add tests for new features
  2. Coverage: Maintain or improve test coverage
  3. Documentation: Update documentation for new features
  4. Security: Follow security best practices
  5. Performance: Consider performance implications
Development Setup
# Clone the repository
git clone https://github.com/itsLeonB/go-crud.git
cd go-crud

# Install dependencies
go mod tidy

# Run tests
make test

# Check coverage
make test-coverage-html

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • GORM: The fantastic ORM library that powers this project
  • Testify: For making tests more readable and maintainable
  • Eris: For enhanced error handling and stack traces

📞 Support


Built with ❤️ for the Go community

Documentation

Overview

Package crud is a generated GoMock package.

Package crud is a generated GoMock package.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BetweenTime

func BetweenTime(col string, start, end time.Time) func(db *gorm.DB) *gorm.DB

BetweenTime returns a GORM scope that filters records between two time values. It uses GetTimeRangeClause to generate the appropriate SQL WHERE clause. Handles open-ended ranges when either start or end time is zero.

func DefaultOrder

func DefaultOrder() func(*gorm.DB) *gorm.DB

DefaultOrder returns a GORM scope that applies default ordering by created_at DESC. This provides consistent ordering for queries that don't specify explicit ordering. Assumes the model has a created_at field.

func ForUpdate

func ForUpdate(enable bool) func(*gorm.DB) *gorm.DB

ForUpdate returns a GORM scope that conditionally adds FOR UPDATE locking to queries. When enable is true, it adds SELECT ... FOR UPDATE to prevent concurrent modifications. Used for pessimistic locking in transaction-critical operations.

func GetTxFromContext

func GetTxFromContext(ctx context.Context) (*gorm.DB, error)

GetTxFromContext retrieves the current GORM transaction from the context. Returns an error if no transaction is found or if the stored value is not a *gorm.DB.

func OrderBy

func OrderBy(field string, ascending bool) func(db *gorm.DB) *gorm.DB

OrderBy returns a GORM scope that orders query results by the specified field. It uses internal.IsValidFieldName to validate the field name and prevent SQL injection. Set ascending to true for ascending order, false for descending.

func Paginate

func Paginate(page, limit int) func(db *gorm.DB) *gorm.DB

Paginate returns a GORM scope that applies pagination to a query. It calculates the appropriate offset based on the page number and limit. The page parameter is 1-indexed (minimum value of 1).

func PreloadRelations

func PreloadRelations(relations []string) func(db *gorm.DB) *gorm.DB

PreloadRelations returns a GORM scope that preloads the specified relations. It eager loads related data to avoid N+1 query problems.

func WhereBySpec

func WhereBySpec[T any](spec T) func(db *gorm.DB) *gorm.DB

WhereBySpec returns a GORM scope that applies a WHERE clause based on the provided struct spec. Non-zero fields in spec will be used as AND conditions in the query.

Types

type BaseEntity added in v0.2.0

type BaseEntity struct {
	ID        uuid.UUID `gorm:"type:uuid;primaryKey;default:uuidv7()"`
	CreatedAt time.Time
	UpdatedAt time.Time `gorm:"autoUpdateTime"`
}

func (BaseEntity) IsZero added in v0.2.0

func (be BaseEntity) IsZero() bool

type MockRepository added in v1.1.0

type MockRepository[T any] struct {
	// contains filtered or unexported fields
}

MockRepository is a mock of Repository interface.

func NewMockRepository added in v1.1.0

func NewMockRepository[T any](ctrl *gomock.Controller) *MockRepository[T]

NewMockRepository creates a new mock instance.

func (*MockRepository[T]) Delete added in v1.1.0

func (m *MockRepository[T]) Delete(ctx context.Context, model T) error

Delete mocks base method.

func (*MockRepository[T]) DeleteMany added in v1.1.0

func (m *MockRepository[T]) DeleteMany(ctx context.Context, models []T) error

DeleteMany mocks base method.

func (*MockRepository[T]) EXPECT added in v1.1.0

func (m *MockRepository[T]) EXPECT() *MockRepositoryMockRecorder[T]

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockRepository[T]) FindAll added in v1.1.0

func (m *MockRepository[T]) FindAll(ctx context.Context, spec Specification[T]) ([]T, error)

FindAll mocks base method.

func (*MockRepository[T]) FindFirst added in v1.1.0

func (m *MockRepository[T]) FindFirst(ctx context.Context, spec Specification[T]) (T, error)

FindFirst mocks base method.

func (*MockRepository[T]) GetGormInstance added in v1.1.0

func (m *MockRepository[T]) GetGormInstance(ctx context.Context) (*gorm.DB, error)

GetGormInstance mocks base method.

func (*MockRepository[T]) Insert added in v1.1.0

func (m *MockRepository[T]) Insert(ctx context.Context, model T) (T, error)

Insert mocks base method.

func (*MockRepository[T]) InsertMany added in v1.1.0

func (m *MockRepository[T]) InsertMany(ctx context.Context, models []T) ([]T, error)

InsertMany mocks base method.

func (*MockRepository[T]) SaveMany added in v1.2.0

func (m *MockRepository[T]) SaveMany(ctx context.Context, models []T) ([]T, error)

SaveMany mocks base method.

func (*MockRepository[T]) Update added in v1.1.0

func (m *MockRepository[T]) Update(ctx context.Context, model T) (T, error)

Update mocks base method.

type MockRepositoryMockRecorder added in v1.1.0

type MockRepositoryMockRecorder[T any] struct {
	// contains filtered or unexported fields
}

MockRepositoryMockRecorder is the mock recorder for MockRepository.

func (*MockRepositoryMockRecorder[T]) Delete added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) Delete(ctx, model any) *gomock.Call

Delete indicates an expected call of Delete.

func (*MockRepositoryMockRecorder[T]) DeleteMany added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) DeleteMany(ctx, models any) *gomock.Call

DeleteMany indicates an expected call of DeleteMany.

func (*MockRepositoryMockRecorder[T]) FindAll added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) FindAll(ctx, spec any) *gomock.Call

FindAll indicates an expected call of FindAll.

func (*MockRepositoryMockRecorder[T]) FindFirst added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) FindFirst(ctx, spec any) *gomock.Call

FindFirst indicates an expected call of FindFirst.

func (*MockRepositoryMockRecorder[T]) GetGormInstance added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) GetGormInstance(ctx any) *gomock.Call

GetGormInstance indicates an expected call of GetGormInstance.

func (*MockRepositoryMockRecorder[T]) Insert added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) Insert(ctx, model any) *gomock.Call

Insert indicates an expected call of Insert.

func (*MockRepositoryMockRecorder[T]) InsertMany added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) InsertMany(ctx, models any) *gomock.Call

InsertMany indicates an expected call of InsertMany.

func (*MockRepositoryMockRecorder[T]) SaveMany added in v1.2.0

func (mr *MockRepositoryMockRecorder[T]) SaveMany(ctx, models any) *gomock.Call

SaveMany indicates an expected call of SaveMany.

func (*MockRepositoryMockRecorder[T]) Update added in v1.1.0

func (mr *MockRepositoryMockRecorder[T]) Update(ctx, model any) *gomock.Call

Update indicates an expected call of Update.

type MockTransactor added in v1.1.0

type MockTransactor struct {
	// contains filtered or unexported fields
}

MockTransactor is a mock of Transactor interface.

func NewMockTransactor added in v1.1.0

func NewMockTransactor(ctrl *gomock.Controller) *MockTransactor

NewMockTransactor creates a new mock instance.

func (*MockTransactor) Begin added in v1.1.0

Begin mocks base method.

func (*MockTransactor) Commit added in v1.1.0

func (m *MockTransactor) Commit(ctx context.Context) error

Commit mocks base method.

func (*MockTransactor) EXPECT added in v1.1.0

EXPECT returns an object that allows the caller to indicate expected use.

func (*MockTransactor) Rollback added in v1.1.0

func (m *MockTransactor) Rollback(ctx context.Context)

Rollback mocks base method.

func (*MockTransactor) WithinTransaction added in v1.1.0

func (m *MockTransactor) WithinTransaction(ctx context.Context, serviceFn func(context.Context) error) error

WithinTransaction mocks base method.

type MockTransactorMockRecorder added in v1.1.0

type MockTransactorMockRecorder struct {
	// contains filtered or unexported fields
}

MockTransactorMockRecorder is the mock recorder for MockTransactor.

func (*MockTransactorMockRecorder) Begin added in v1.1.0

func (mr *MockTransactorMockRecorder) Begin(ctx any) *gomock.Call

Begin indicates an expected call of Begin.

func (*MockTransactorMockRecorder) Commit added in v1.1.0

func (mr *MockTransactorMockRecorder) Commit(ctx any) *gomock.Call

Commit indicates an expected call of Commit.

func (*MockTransactorMockRecorder) Rollback added in v1.1.0

func (mr *MockTransactorMockRecorder) Rollback(ctx any) *gomock.Call

Rollback indicates an expected call of Rollback.

func (*MockTransactorMockRecorder) WithinTransaction added in v1.1.0

func (mr *MockTransactorMockRecorder) WithinTransaction(ctx, serviceFn any) *gomock.Call

WithinTransaction indicates an expected call of WithinTransaction.

type Repository added in v1.0.0

type Repository[T any] interface {
	// Insert creates a new record in the database.
	Insert(ctx context.Context, model T) (T, error)
	// FindAll retrieves multiple records based on the specification.
	FindAll(ctx context.Context, spec Specification[T]) ([]T, error)
	// FindFirst retrieves the first record matching the specification.
	FindFirst(ctx context.Context, spec Specification[T]) (T, error)
	// Update modifies an existing record in the database.
	Update(ctx context.Context, model T) (T, error)
	// Delete removes a record from the database (hard delete).
	Delete(ctx context.Context, model T) error
	// InsertMany creates multiple records in a single database operation.
	InsertMany(ctx context.Context, models []T) ([]T, error)
	// DeleteMany removes multiple records in a single database operation (hard delete).
	DeleteMany(ctx context.Context, models []T) error
	// SaveMany saves multiple records in a single database operation.
	SaveMany(ctx context.Context, models []T) ([]T, error)
	// GetGormInstance returns the appropriate GORM DB instance (transaction-aware).
	GetGormInstance(ctx context.Context) (*gorm.DB, error)
}

Repository defines a generic interface for basic CRUD operations on entities of type T. It provides standard database operations with context support and transaction awareness. The interface abstracts the underlying database implementation for easier testing and flexibility.

func NewRepository added in v1.0.0

func NewRepository[T any](db *gorm.DB) Repository[T]

NewRepository creates a new CRUD repository implementation using GORM. The repository provides transaction-aware database operations for the specified entity type T.

type Specification

type Specification[T any] struct {
	Model            T        // Model with fields set for WHERE conditions
	PreloadRelations []string // Relations to eager load
	ForUpdate        bool     // Whether to use SELECT ... FOR UPDATE
}

Specification defines query parameters for database operations. It includes the model for WHERE conditions, relations to preload, and locking options.

type Transactor

type Transactor interface {
	// Begin starts a new database transaction and returns a context containing the transaction.
	Begin(ctx context.Context) (context.Context, error)
	// Commit commits the current transaction in the context.
	Commit(ctx context.Context) error
	// Rollback rolls back the current transaction in the context without returning an error.
	Rollback(ctx context.Context)
	// WithinTransaction executes a service function within a database transaction.
	WithinTransaction(ctx context.Context, serviceFn func(ctx context.Context) error) error
}

Transactor provides an interface for managing database transactions with context. It abstracts transaction operations to allow for easier testing and different implementations.

func NewTransactor

func NewTransactor(db *gorm.DB) Transactor

NewTransactor creates a new Transactor implementation using GORM. The returned Transactor can be used to manage database transactions with context propagation.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL