dqk

package module
v0.0.15 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2025 License: Apache-2.0 Imports: 14 Imported by: 0

README

DynamicQueryKit (DQK) 🧠⚡️

A modular, lightweight Go library to help you build powerful, flexible APIs with dynamic SQL filtering, clean decoding, permission logic, and plug-and-play middlewares.


✨ Features

  • ✅ Decode request bodies (JSON & XML) with detailed error handling
  • ✅ Build dynamic SQL filters using Squirrel
  • ✅ Middleware: CORS, Logging, Middleware stacking
  • ✅ Permission-based access control with scoped and global checks
  • ✅ Pagination helper that wraps Squirrel queries
  • ✅ Idiomatic Go, clean API, testable and composable components

📦 Installation

go get github.com/yourusername/dynamicquerykit

🔍 Quick Examples

🔸 DecodeBody
type User struct {
	ID   int    `json:"id" xml:"id"`
	Name string `json:"name" xml:"name"`
}

var user User
status, err := dynamicquerykit.DecodeBody("application/json", r.Body, &user)

Supports JSON & XML decoding with descriptive HTTP error codes and messages.


🔸 DataEncode

Respond dynamically based on Accept header (JSON, XML).

data := map[string]string{"message": "hello"}
bytes, contentType, _ := dynamicquerykit.DataEncode("application/json", data)
w.Header().Set("Content-Type", contentType)
w.Write(bytes)

🔸 Permission Utilities
perms := dynamicquerykit.BuildPermissionSet([]string{"view_users", "edit_users"})

if dynamicquerykit.HasPermission("edit_users", perms) {
	// allow edit
}

if dynamicquerykit.IsAccessRestricted("view_users", "view_all_users", perms) {
	http.Error(w, "forbidden", http.StatusForbidden)
}

🔸 Pagination Helper (Squirrel)
query := squirrel.Select("id").From("cars").Where(squirrel.Eq{"color": "black"})
total, err := dynamicquerykit.GetPaginationData(ctx, db, &query)

Wraps the query in a SELECT COUNT(*) FROM (...) for total count pagination support.


🔸 Middleware Stack
stack := dynamicquerykit.CreateStack(
	dynamicquerykit.Cors,
	dynamicquerykit.Logging,
)

http.Handle("/", stack(http.HandlerFunc(myHandler)))

or even better. Add the handlers to your server mux

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /v1/health", controllers.GetHealth)

	middlewareStack := dqk.CreateStack(dqk.Logging, dqk.Cors)

	server := http.Server{
        Addr:    ":8080",
		Handler: middlewareStack(mux),
	}

	server.ListenAndServe()
}

Use CreateStack to compose middlewares easily, including built-in Logging and Cors.


🧠 Philosophy

DynamicQueryKit is designed to be:

  • Minimal: No unnecessary dependencies
  • Composable: Everything is made to work together or independently
  • Practical: Targets real problems when building APIs in Go. Especially dynamic queries

Example workflow

  • Specify filters
filters := []dqk.filers{
    {Name: "color", Operator: "IN", DbField: "colors.name", FieldID: "colors.id"},                                                      // added FieldID
    // ... any other columns
}
  • Make your base query dqk uses squirrel for query building
myquery := sq.Select("id,name").From("cloths as c").Join("colors as cl ON cl.id = c.color_id")
  • Get user request parameters -> params["color"] = ["blue","red"]
  • Pass the params and the base query with your filters to dqk.DynamicFilters
query, _ := dqk.DynamicFilters(filters, myquery, r.URL.Query()) //or pass your params variable

// query = sq.Select("id,name").From("cloths as c").Join("colors as cl ON cl.id = c.color_id").Where("color IN (blue,red)")
  • (optional) if you want pagination
paginationQuery := dqk.GetPaginationQuery(query)
// do not limit the pagination query, you will only get the number you specified as limit
query = query.limit(mylimit).offset(myoffset)
query = query.OrderBy(dqk.OrderValidation(r.URL.Query().get("order_by"),r.URL.Query().Get("order_direction"),filters))
// Optionally you can specify nulls last
// query = query.OrderBy(dqk.OrderValidation(r.URL.Query().get("order_by"),r.URL.Query().Get("order_direction"),filters + " NULLS LAST"))

Documentation

Overview

Package dqk provides helper functions to manage: Caching with memcached, dynamic filter for queries, standard responses for pagination, deleted/updated/created assets and deleted cache provides distinct fields struct and an id that can be used for faster lookups using the index of each field. Helper methods that generate cache keys based on applied filters helper functions that execute database queries for update/delete/update and helper methods for pagination both for manual query definition and automatically by wrapping your main query in a CTE. database validation methods for user friendly messaged that can be shown as a response

Index

Constants

View Source
const (
	TokenLimit  = "limit"
	TokenOffset = "offset"
	TokenWhere  = "where"
	TokenHaving = "having"
)

Variables

This section is empty.

Functions

func AcceptedEncoding

func AcceptedEncoding(accept string) string

AcceptedEncoding returns a default encoding if the one specified does not match the specification

func AreFiltersAggregate

func AreFiltersAggregate(filters []Filters) bool

AreFiltersAggregate returns true if any of the provided filters use an allowedAggregateFunctions

func BuildPermissionSet

func BuildPermissionSet(userPermissions []string) map[string]bool

BuildPermissionSet constructs a map-based set from a slice of permission strings, allowing for efficient O(1) permission lookups.

Each string in the input slice is used as a map key with a value of true. This is commonly used to preprocess user permission lists into a format suitable for fast lookup when checking access.

Example:

BuildPermissionSet([]string{"view:users", "edit:users"})
// → map[string]bool{"view:users": true, "edit:users": true}

func Cors

func Cors(next http.Handler) http.Handler

Cors adds CORS to all routes in the app

func DataEncode

func DataEncode(Accept string, Data any) ([]byte, string, error)

DataEncode encodes structs to json and xml provides an easily scalable way to support extra encodings accross the api Returns the Data encoded, returns the Content-Type of the encoded data and error

func DatabaseValidation

func DatabaseValidation(err error) (int, error)

DatabaseValidation checks a database error and returns an appropriate error message and status code that can be directly used in the response. The underline error messaages is always logged.

func DecodeBody

func DecodeBody(contentType string, body io.Reader, data any) (int, error)

DecodeBody gets the body of a request and parses it

func DynamicFilters

func DynamicFilters(f []Filters, q sq.SelectBuilder, queryParams map[string][]string) sq.SelectBuilder

DynamicFilters it applies dynamic filters based on the allowed filters. These are added to the specified query it can get the query params as is from the r.URL.query() method. it does not stop the user from passing multiple = params all conditions are passed as AND parameters. This is true for both having & where conditions

func GetBool

func GetBool(key string, fallback bool) bool

GetBool gets env variable of a bool and if missing sets a default value

func GetDate

func GetDate(key string, fallback time.Time) time.Time

GetDate gets a time.DateOnly string and makes it time.Time

func GetInt

func GetInt(key string, fallback int) int

GetInt gets env variable of a int and if missing sets a default value

func GetPaginationQuery

func GetPaginationQuery(q sq.SelectBuilder) sq.SelectBuilder

GetPaginationQuery provides a query that can be used for pagination. it wraps the provided query as a subquery.

func GetParamsApplied added in v0.0.13

func GetParamsApplied(filters []Filters, params map[string][]string) map[string]string

func GetRouteKey

func GetRouteKey(route string, assetID *int, args ...string) (string, string)

GetRouteKey returns the Route key which is used in the GetCacheKey method and the correct Index Key which is going to be used in the SetKeyIndex Method. The Index Key will store in a json list the Route Key ie. -> properties,1 will return properties:1 for both Route Key & Index Key ie. -> properties,1,google_searches will return properties:1:google_searches for Route Key and properties:1 Index Key Provides an easier way to handle cache keys for subresources of dynamic routes ie. properties/{some_id}/google_searches When accessing stuff in deeper levels than that it is recommended to reuse the same method ,keeping the Route Key and using the first level Index Key ie. -> properties/{some_id}/google_searches/{some_other_id} should keep the index key of properties:some_id but routeKey would be properties:some_id:google_searches:some_other_id returned by GetRouteKey

func GetString

func GetString(key string, fallback string) string

GetString gets env variable of a string and if missing sets a default value

func GetTime

func GetTime(key string, fallback time.Duration) time.Duration

GetTime Gets env variable of a key, makes it into time.duration else returns the fallback

func HasPermission

func HasPermission(access string, perms map[string]bool) bool

HasPermission checks whether the user has a specific permission. It takes a permission key (e.g., "view:users") and a map of the user's permissions, where each key represents a granted permission and the value is typically true.

Returns true if the permission key exists in the map and is set to true. Returns false if the permission is missing or explicitly set to false.

Example:

perms := BuildPermissionSet([]string{"view:users", "edit:posts"})
HasPermission("view:users", perms) // true
HasPermission("delete:users", perms) // false

func IsAccessRestricted

func IsAccessRestricted(UserPermissions map[string]bool, basePerm, elevatedPerm string) bool

IsAccessRestricted determines whether the user lacks both the base and elevated permissions required to access a resource.

This is useful in scenarios where you want to allow users with either scoped ("view") or broad ("viewAll") permissions to access a resource.

Returns true if the user has neither permission. Returns false if the user has at least one of them.

Example:

IsAccessRestricted("view:article", "viewAll:articles", userPerms)
// true → access denied
// false → access granted

func IsFieldJSONTag

func IsFieldJSONTag(dataStruct any, strField string) bool

IsFieldJSONTag checks if the string provided matches any json tag of a struct it requires the struct provided has json tags specified

func Logging

func Logging(next http.Handler) http.Handler

Logging Provides logging middleware for the app

func OrderValidation

func OrderValidation(orderByStr string, orderDirectionStr string, filters []Filters) string

OrderValidation provide a struct, the order by string and d.irection and default order by string

func ValidateParams added in v0.0.13

func ValidateParams(filters []Filters, params map[string][]string) map[Filters][]string

func ValidateValus added in v0.0.13

func ValidateValus(filterValues map[Filters][]string) map[Filters][]string

Types

type Conditional added in v0.0.14

type Conditional struct {
	Expresion sq.Sqlizer
	Type      string
	Values    []string
}

func BuildFilterConditions

func BuildFilterConditions(filters []Filters, params map[string][]string) []Conditional

BuildFilterConditions takes in allowed filters and values to be filtered. The key of the values map must match the Filter.Name field. It returns first all where conditions (conditions that should be added in a where claus) and having conditions (all conditions that should be added in a having claus) Both can be consolidated using either sq.And() or sq.Or() or a custom method in order to be applied to a filter

func NewConditional added in v0.0.14

func NewConditional(exrp sq.Sqlizer, ConType string, values []string) Conditional

func (*Conditional) Apply added in v0.0.14

type CreatedAssetResponse

type CreatedAssetResponse struct {
	Status    int       `json:"status" xml:"status" yaml:"status" csv:"status"`
	AssetID   *int64    `json:"asset_id" xml:"asset_id" yaml:"asset_id" csv:"asset_id"`
	CreatedAt time.Time `json:"date" xml:"date" yaml:"date" csv:"date"`
}

CreatedAssetResponse generic response when creating a new asset

type DeletedCacheResponse

type DeletedCacheResponse struct {
	Status      int `json:"status" xml:"status" yaml:"status" csv:"status"`
	KeysFlushed int `json:"keys_flushed" xml:"keys_flushed" yaml:"keys_flushed" csv:"keys_flushed"`
}

DeletedCacheResponse standard response for routes that delete cache

type DistinctFieldNames

type DistinctFieldNames struct {
	ID    string  `json:"id" xml:"id" yaml:"id" csv:"id"`
	Name  *string `json:"name" xml:"name" yaml:"name" csv:"name"`
	Count *int    `json:"count" xml:"count" yaml:"count" csv:"count"`
}

DistinctFieldNames is the basic component of DistinctFieldNamesResponse

type DistinctFieldNamesResponse

type DistinctFieldNamesResponse struct {
	Status int                  `json:"status" xml:"status" yaml:"status" csv:"status"`
	Total  int                  `json:"total" xml:"total" yaml:"total" csv:"total"`
	Data   []DistinctFieldNames `json:"data" xml:"data" yaml:"data" csv:"data"`
}

DistinctFieldNamesResponse response for when selecting a field from a route

type ErrorResponse

type ErrorResponse struct {
	Status  int    `json:"status" xml:"status" yaml:"status" csv:"status"`
	Message string `json:"message" xml:"message" yaml:"message" csv:"message"`
}

ErrorResponse standard for errors

type Filters

type Filters struct {
	Name     string `json:"name" xml:"name" yaml:"name" csv:"name"`
	Operator string `json:"operator" xml:"operator" yaml:"operator" csv:"operator"`
	DbField  string `json:"db_field" xml:"db_field" yaml:"db_field" csv:"db_field"`
	FieldID  string `json:"field_id" xml:"field_id" yaml:"field_id" csv:"field_id"`
}

Filters standard for allowed filters

func ExtendFilters

func ExtendFilters(filters [][]Filters) []Filters

ExtendFilters takes in n filters and returns a complete filter list

func IsFieldFilter

func IsFieldFilter(filters []Filters, field string) (bool, Filters)

IsFieldFilter returns true if a string field matches the name of any filter in the provided filter slice

func (*Filters) ApplyNullToken added in v0.0.13

func (f *Filters) ApplyNullToken(values ...string) bool

func (*Filters) HasNullOrNotNull added in v0.0.13

func (f *Filters) HasNullOrNotNull(values ...string) bool

func (*Filters) IsAggregate added in v0.0.13

func (f *Filters) IsAggregate() bool

type IDs

type IDs struct {
	ID int `json:"id" xml:"id" yaml:"id" csv:"id"`
}

IDs generic struct for getting the IDs of assets

type Middleware

type Middleware func(http.Handler) http.Handler

Middleware takes in a middleware

func CreateStack

func CreateStack(xs ...Middleware) Middleware

CreateStack provides a simple way to stack middlewares

type Pagination

type Pagination struct {
	TotalAssets int  `json:"total_assets" xml:"total_assets" yaml:"total_assets" csv:"total_assets"`
	CurrentPage int  `json:"current_page" xml:"current_page" yaml:"current_page" csv:"current_page"`
	TotalPages  int  `json:"total_pages" xml:"total_pages" yaml:"total_pages" csv:"total_pages"`
	NextPage    bool `json:"next_page" xml:"next_page" yaml:"next_page" csv:"next_page"`
	Limit       int  `json:"limit" xml:"limit" yaml:"limit" csv:"limit"`
}

Pagination standard for pagination

Directories

Path Synopsis
Package caching provides an easy abstraction layer for adding caching to an application
Package caching provides an easy abstraction layer for adding caching to an application

Jump to

Keyboard shortcuts

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