(Feat): Initial Commit
This commit is contained in:
126
internal/util/db/json.go
Normal file
126
internal/util/db/json.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/aarondl/sqlboiler/v4/queries/qm"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
const (
|
||||
whereJSONMaxLevel = 10
|
||||
)
|
||||
|
||||
// WhereJSON constructs a QueryMod for querying a JSONB column.
|
||||
//
|
||||
// The filter interface provided is inspected using reflection, all fields with
|
||||
// a (non-empty) `json` tag will be added to the query and combined using `AND` -
|
||||
// fields tagged with `json:"-"` will be ignored as well. Alternatively, a string
|
||||
// can be provided, performing a string comparison with the database value (the
|
||||
// stored JSON value does not necessarily have to be a string, but could be an
|
||||
// integer or similar). The `json` tag's (first) value will be used as the "key"
|
||||
// for the query, allowing for field renaming or different capitalizations.
|
||||
//
|
||||
// At the moment, the root level `filter` value must either be a struct or a string.
|
||||
// WhereJSON will panic should it encounter a type it cannot process or the filter
|
||||
// provided results in an empty QueryMod - this allows for easier call chaining
|
||||
// at the expense of panics in case of incorrect filters being passed.
|
||||
//
|
||||
// WhereJSON should support all basic types as well as pointers and array/slices
|
||||
// of those out of the box, given the Postgres driver can handle their serialization.
|
||||
// nil pointers are skipped automatically.
|
||||
// At the moment, struct fields are only supported for composition purposes: if a
|
||||
// struct is encountered, WhereJSON recursively traverses it (up to 10 levels deep)
|
||||
// and adds all eligible fields to the top level query.
|
||||
// Should an array or slice be encountered, their values will be added using the
|
||||
// `<@` JSONB operator, checking whether all entries existx at the top level within
|
||||
// the JSON column.
|
||||
// At the time of writing, no support for special database/HTTP types such as the
|
||||
// `null` or `strfmt` packages exists - use their respective base types instead.
|
||||
//
|
||||
// Whilst WhereJSON was designed to be used with Postgres' JSONB column type, the
|
||||
// current implementation also supports the JSON type as long as the filter struct
|
||||
// does not contain any arrays or slices. Note that this compatibility might change
|
||||
// at some point in the future, so it is advised to use the JSONB data type unless
|
||||
// your requirements do not allow for it.
|
||||
func WhereJSON(table string, column string, filter interface{}) qm.QueryMod {
|
||||
qms := whereJSON(table, column, filter, 0)
|
||||
if len(qms) == 0 {
|
||||
panic(errors.New("filter resulted in empty query"))
|
||||
}
|
||||
|
||||
return qm.Expr(qms...)
|
||||
}
|
||||
|
||||
func whereJSON(table string, column string, filter interface{}, level int) []qm.QueryMod {
|
||||
if level >= whereJSONMaxLevel {
|
||||
panic(fmt.Errorf("whereJSON reached maximum recursion (%d/%d)", level, whereJSONMaxLevel))
|
||||
}
|
||||
|
||||
qms := make([]qm.QueryMod, 0)
|
||||
|
||||
filterType := reflect.TypeOf(filter)
|
||||
switch filterType.Kind() {
|
||||
case reflect.Struct:
|
||||
filterValue := reflect.ValueOf(filter)
|
||||
for i := 0; i < filterType.NumField(); i++ {
|
||||
field := filterType.Field(i)
|
||||
|
||||
// skip unexported fields as we cannot retrieve their values
|
||||
if len(field.PkgPath) != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jsonkey := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||
if jsonkey == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
filterValueField := filterValue.Field(i)
|
||||
if filterValueField.Kind() != reflect.Struct && jsonkey == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
isArray := false
|
||||
var val interface{}
|
||||
switch filterValueField.Kind() {
|
||||
case reflect.Struct:
|
||||
qms = append(qms, whereJSON(table, column, filterValueField.Interface(), level+1)...)
|
||||
continue
|
||||
case reflect.Ptr:
|
||||
if !filterValueField.IsValid() || filterValueField.IsNil() {
|
||||
continue
|
||||
}
|
||||
if filterValueField.Elem().Kind() == reflect.Array ||
|
||||
filterValueField.Elem().Kind() == reflect.Slice {
|
||||
isArray = true
|
||||
}
|
||||
val = filterValueField.Elem().Interface()
|
||||
case reflect.Array,
|
||||
reflect.Slice:
|
||||
if !filterValueField.IsValid() || filterValueField.IsNil() {
|
||||
continue
|
||||
}
|
||||
isArray = true
|
||||
val = filterValueField.Interface()
|
||||
default:
|
||||
val = filterValueField.Interface()
|
||||
}
|
||||
|
||||
if isArray {
|
||||
qms = append(qms, qm.Where(fmt.Sprintf("%s.%s->'%s' <@ to_jsonb(?::text[])", table, column, jsonkey), pq.Array(val)))
|
||||
} else {
|
||||
qms = append(qms, qm.Where(fmt.Sprintf("%s.%s->>'%s' = ?", table, column, jsonkey), val))
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
qms = append(qms, qm.Where(fmt.Sprintf("%s.%s::text = ?", table, column), filter))
|
||||
default:
|
||||
panic(fmt.Errorf("invalid filter type %v", filterType.Kind()))
|
||||
}
|
||||
|
||||
return qms
|
||||
}
|
||||
Reference in New Issue
Block a user