query

package
v0.0.0-...-c9062c8 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: AGPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package query provides tag-driven request binding, GORM scopes, pagination, and data access helpers for the API layer.

Tag contract for query structs:

query:"name"     query parameter name, required for a field to
                 participate in Bind; also used to locate the sort_dir
                 companion field in Scopes
default:"25"     value used when the parameter is absent
minimum:"1"      lower clamp applied after parsing (int fields)
maximum:"200"    upper clamp applied after parsing (int fields)
filter:"column"  adds WHERE column = ? when the field is non-empty
search:"a,b"     adds (a ILIKE ? OR b ILIKE ?) with %value% when non-empty
sort:"a,b"       adds ORDER BY when the value appears in the allowed list
default_sort:"c [asc|desc]"
                 adds ORDER BY c when the sort value is empty or not in
                 the allowed list; a missing direction means ASC

The query, default, minimum, and maximum tags match the names and semantics that github.com/danielgtaylor/huma/v2 reads, so the same structs can also be bound by huma. The filter, search, and sort tags are project-specific and only interpreted by Scopes.

An absent query parameter and a parameter present with an empty value are treated the same way: the default tag applies, otherwise the zero value is kept.

Index

Constants

This section is empty.

Variables

View Source
var ErrConflict = errors.New("the value conflicts with an existing record or a database constraint")

ErrConflict is returned by Insert and Update when the database rejects the write because of a unique index or check constraint violation. The underlying GORM error is not preserved, so detect it with errors.Is.

View Source
var ErrNotFound = gorm.ErrRecordNotFound

ErrNotFound is returned by the single-record helpers when no matching row exists. It is the gorm.ErrRecordNotFound value itself, so errors.Is works against either sentinel.

Functions

func Bind

func Bind(target any, c *echo.Context) error

Bind fills the exported fields of target (a pointer to a struct) from the request query string, driven by the query/default/minimum/maximum tags.

Supported field kinds are string, bool, and signed int kinds. A parse failure returns an error naming the parameter so handlers can map it to a 400 response. Absent or empty parameters take the default tag value when present, otherwise the field keeps its zero value. Fields without a query tag, unexported fields, unsupported kinds, and unknown query parameters are ignored.

func BodyMap

func BodyMap(body any, omit ...string) (map[string]any, error)

BodyMap marshals body to JSON and back into a map[string]any, then deletes the "id" key and every key listed in omit. The map form keeps zero values in the update payload, which GORM's struct-based Updates would skip.

func Delete

func Delete[T any](db *gorm.DB, column string, value any, hard bool, scopes ...func(*gorm.DB) *gorm.DB) error

Delete removes the record matching column = value. The record is probed first so a missing row yields ErrNotFound instead of a silent 0-rows delete. When hard is true both the probe and the delete run Unscoped so soft-deleted rows can be targeted and removed permanently. Scopes apply to the probe only.

func Get

func Get[T any](db *gorm.DB, column string, value any, scopes ...func(*gorm.DB) *gorm.DB) (*T, error)

Get fetches a single record of T by matching column = value. Scopes are applied to the lookup. Returns ErrNotFound when no row matches, other errors verbatim.

func Insert

func Insert[T any](db *gorm.DB, item *T) error

Insert creates a new record. Returns ErrConflict on a duplicate-key or check-constraint violation, other errors verbatim.

func Restore

func Restore[T any](db *gorm.DB, column string, value any) (*T, error)

Restore clears the deleted_at column of a soft-deleted record. The lookup and the update both run Unscoped so the row can be found and the column rewritten to NULL. UpdateColumn (not Update) keeps GORM's BeforeUpdate callbacks from touching updated_at. Returns the re-read record, or ErrNotFound when no row matches.

func Scopes

func Scopes(q any) []func(*gorm.DB) *gorm.DB

Scopes converts the filter/search/sort tags of a bound query struct into GORM scopes. String fields only; empty values contribute nothing, and a sort value outside the allowed list falls back to the default_sort tag when one is present.

filter:"column"  WHERE column = ?
search:"a,b"     (a ILIKE ? OR b ILIKE ?) with %value%
sort:"a,b"       ORDER BY <value> ASC|DESC when the value is in the list
default_sort:"c [asc|desc]"
                 ORDER BY used when sort_by is empty or not in the list;
                 a missing direction means ASC

The sort direction comes from the string field tagged query:"sort_dir" on the same struct: "desc" (case-insensitive) yields DESC, anything else yields ASC. Missing sort_dir fields default to ASC.

Column names come from developer-written tags and are used verbatim. Scopes panics when q is not a struct or a non-nil pointer to one; that is a developer error at startup.

func Update

func Update[T any](db *gorm.DB, column string, value any, updates map[string]any, scopes ...func(*gorm.DB) *gorm.DB) (*T, error)

Update first verifies the record exists (scopes applied), then applies the updates map with a WHERE column = ? guard so a changed primary key in the payload can never touch unrelated rows. Returns the re-read record, or ErrNotFound, ErrConflict, or other errors verbatim.

Types

type PaginatedResponse

type PaginatedResponse[T any] struct {
	Items      []T   `json:"items"`
	Page       int   `json:"page"`
	PageSize   int   `json:"page_size"`
	TotalItems int64 `json:"total_items"`
	TotalPages int   `json:"total_pages"`
	HasNext    bool  `json:"has_next"`
	HasPrev    bool  `json:"has_prev"`
}

PaginatedResponse wraps a slice of any model with pagination metadata.

func Paginate

func Paginate[T any](db *gorm.DB, page, pageSize int, scopes ...func(*gorm.DB) *gorm.DB) (PaginatedResponse[T], error)

Paginate runs a paginated list query for model T and returns the page of results plus pagination metadata. page < 1 becomes 1, pageSize < 1 becomes 25, and pageSize > 200 becomes 200.

The count relies on GORM's Count deleting the ORDER BY clause (when there is no GROUP BY), so scopes must be applied eagerly here rather than via .Scopes(). Deferred scopes only run inside Execute and would add their ORDER BY after Count already stripped the clause, which Postgres rejects in a count over an aggregated SELECT. The Find then runs with Offset/Limit on the same chain.

Jump to

Keyboard shortcuts

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