Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions v2/dbutils/ops/bobops/bobops.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ func NewBobFilterMap(fields map[string]string) ops.FilterMap[bob.Mod[*dialect.Se
type BobFilterer struct{}

func (b *BobFilterer) ParseFilter(filter, alias string, op string, rawValue string, having bool) (bob.Mod[*dialect.SelectQuery], string, interface{}, error) {
return parseFilter(filter, alias, op, rawValue, having)
}

func (b *BobFilterer) ParseSorting(sortList []string) (bob.Mod[*dialect.SelectQuery], error) {
return sm.OrderBy(strings.Join(sortList, ", ")), nil
}

func parseFilter(filter, alias string, op string, rawValue string, having bool) (bob.Mod[*dialect.SelectQuery], string, interface{}, error) {
if having {
if ops.IsUnaryOp(op) {
q := strings.ReplaceAll(filter, "{}", alias)
Expand Down Expand Up @@ -56,7 +64,3 @@ func (b *BobFilterer) ParseFilter(filter, alias string, op string, rawValue stri
q := strings.ReplaceAll(filter, "{}", alias)
return sm.Where(psql.Raw(q, rawValue)), q, rawValue, nil
}

func (b *BobFilterer) ParseSorting(sortList []string) (bob.Mod[*dialect.SelectQuery], error) {
return sm.OrderBy(strings.Join(sortList, ", ")), nil
}
90 changes: 90 additions & 0 deletions v2/dbutils/ops/gen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Filter Code Generator

This package provides automatic code generation for database filter methods based on struct field comments.

## Overview

Instead of manually creating `FilterMap` instances and calling `AddFilters` for each API filter, you can now:

1. Define your endpoint payload struct with special `db:filter` comments
2. Run `go generate` to automatically create `AddFilters` methods
3. Use the generated methods in your handlers

## Usage

### 1. Annotate your structs

Add `db:filter` comments to fields that should be filterable:

```go
type ListDCRsRequest struct {
// db:filter bob_gen.ColumnNames.DCRS.Type
Type string `query:"type"`
// db:filter bob_gen.ColumnNames.DCRS.Status
Status string `query:"status"`
// db:filter bob_gen.ColumnNames.DCRS.CreatedBy
CreatedBy *string `query:"created_by"`
// db:filter bob_gen.ColumnNames.DCRS.Tags
Tags []string `query:"tags"`

// Regular fields without filter comments are ignored
Limit int `query:"limit"`
Offset int `query:"offset"`
}
```
Of course, this is assuming Huma. There is no support for Goa, sorry.


### 2. Add go generate directive

Add this line to the top of your model files (it will also work in main.go, only a bit slower):

```go
//go:generate go run github.com/top-solution/go-libs/v2/dbutils/ops/gen/cmd bob .
```

Or use the command directly:

```bash
# Scan all folders inside ., generate bob filters
go run github.com/top-solution/go-libs/v2/dbutils/ops/gen/cmd bob .

# Scan specific package, generate boiler filters
go run github.com/top-solution/go-libs/v2/dbutils/ops/gen/cmd boiler path/to/specific/packagh
```

### 3. Run go generate

```bash
go generate ./...
```

Using ./.. will make sure it's going to also run //go:generate directive inside your model files.

### 4. Use the generated methods

The generator creates an `AddFilters` method for each annotated struct:

```go
func (r *ListDCRsRequest) AddFilters(q *[]bob.Mod[*dialect.SelectQuery]) error
```

Use it in your handlers:

```go
func ListDCRsHandler(ctx context.Context, req *ListDCRsRequest) (*ListDCRsResponse, error) {
var query []bob.Mod[*dialect.SelectQuery]

// Automatically add filters based on request fields
if err := req.AddFilters(&query); err != nil {
return nil, err
}

// Add other query modifications
query = append(query, sm.Limit(req.Limit))

// Execute query
dcrs, err := models.DCRS(query...).All(ctx, db)
// ...
}
```
99 changes: 99 additions & 0 deletions v2/dbutils/ops/gen/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/top-solution/go-libs/v2/dbutils/ops/gen"
)

func main() {
if len(os.Args) < 3 {
log.Fatal("Usage: gen <filter_type> <root_path>")
}

filterType := os.Args[1]
rootPath := os.Args[2]

// Convert relative path to absolute for better handling
absRootPath, err := filepath.Abs(rootPath)
if err != nil {
log.Fatalf("Failed to get absolute path for %s: %v", rootPath, err)
}

fmt.Printf("Scanning directory: %s\n", absRootPath)

// Walk through all directories under the root path
err = filepath.Walk(absRootPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

// Skip if not a directory
if !info.IsDir() {
return nil
}

// Skip hidden directories and vendor directories, but allow the root directory even if it starts with "."
if path != absRootPath && (strings.HasPrefix(info.Name(), ".") || info.Name() == "vendor") {
return filepath.SkipDir
}

// Check if this directory contains Go files (excluding test and generated files)
hasGoFiles, err := hasRelevantGoFiles(path)
if err != nil {
return err
}

if !hasGoFiles {
return nil
}

// Get package name from directory name
packageName := filepath.Base(path)

// Handle special case where the directory is "." or the root
if packageName == "." || path == absRootPath {
// Try to get package name from go.mod or use directory name
if wd, err := os.Getwd(); err == nil {
packageName = filepath.Base(wd)
}
}

// Create generator and process the package
generator := gen.NewGenerator(packageName, path, filterType)
if err := generator.GenerateFromPackage(); err != nil {
log.Printf("Warning: Failed to generate filters for package %s: %v", path, err)
return nil // Continue processing other packages
}

return nil
})

if err != nil {
log.Fatalf("Failed to walk directory tree: %v", err)
}

fmt.Println("Filter generation completed.")
}

// hasRelevantGoFiles checks if a directory contains Go files that are not test files or generated files
func hasRelevantGoFiles(dir string) (bool, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.go"))
if err != nil {
return false, err
}

for _, file := range files {
filename := filepath.Base(file)
// Skip test files and generated files
if !strings.HasSuffix(filename, "_test.go") && !strings.Contains(filename, "_gen.go") && !strings.Contains(filename, ".gen.go") {
return true, nil
}
}

return false, nil
}
Loading