query

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Aug 7, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const (
	DefaultPage    = 1
	DefaultPerPage = 20
	MaxPerPage     = 100
	MaxPage        = 10_000
)
View Source
const CountAlias = "agg_count"

CountAlias is the SQL column alias COUNT(*) is returned under.

Variables

View Source
var ErrAggForbiddenField = errors.New("aggregate references a field not permitted for this role")

ErrAggForbiddenField signals that an aggregate referenced a field the role may not read (its RBAC field allowlist excludes it). Callers map this to 403; any other BuildAggregate error is a 400. Aggregating a hidden field would otherwise leak its values, so it is forbidden — the no-leak-via-aggregate guarantee (G3).

Functions

func AppendAliasedRowCondition

func AppendAliasedRowCondition(sql string, args []any, alias string, cond *rbac.WhereCondition) (string, []any, error)

AppendAliasedRowCondition is AppendRowCondition for a statement that JOINs more than one table, so the condition column must be qualified by its table alias to be unambiguous (the relation subresource route: SELECT r.* FROM <target> r JOIN <parent> src …). alias is an engine-controlled identifier (e.g. "r"), never user input; the condition field is validated as a bare identifier and the value is always a bound parameter. The operator is equality — the only operator an RBAC condition may declare (enforced at schema load; see schema.validateConditionOp).

func AppendRowCondition

func AppendRowCondition(sql string, args []any, cond *rbac.WhereCondition) (string, []any, error)

AppendRowCondition appends a row-level RBAC WhereCondition to a single-row SQL statement that is already parameterized at $1..$len(args) (e.g. a GET-by-id or DELETE filtered on id). The condition field is validated as a bare identifier (the value is always a bound parameter); an invalid field returns an error so the caller fails closed. This is the one canonical implementation used by both the REST and GraphQL get-by-id/delete paths so they enforce the SAME row-level RBAC the list path applies via buildWhere.

func HasInclude

func HasInclude(include string) bool

HasInclude reports whether the request opted into relation embedding. Kept tiny so the no-include hot path is a single empty-string check at the call site.

func ParseCountFlag

func ParseCountFlag(params url.Values) (bool, error)

ParseCountFlag reads the ?count flag by VALUE (ENG-23/ENG-18, ADR-024 §4). The flag used to be tested for presence only — `?count=false` and `?count=0` turned the total ON — and REST disagreed with GraphQL, whose count argument is a real Boolean. Accepted: bare `?count` / `?count=` (on — the documented syntax), true/1 (on), false/0 (off), case-insensitive; anything else is an error naming the value and the accepted set. Absent ⇒ off. A repeated count is rejected like every other engine-owned parameter.

func RejectListParams

func RejectListParams(params url.Values, route string, alsoInclude bool) error

RejectListParams returns a named error when params carry an engine-owned LIST parameter (or a filter[/order[ prefix) on a route that would silently discard it (NIGHT-SWEEP-S1 audit finding, the ENG-14/ADR-024 class on the single-record and SSE surfaces): GET /api/notes/{id}?filter[status][eq]=paid used to return the row REGARDLESS of the filter — the caller believed they performed a conditional read — while the identical parameter on the list route is a named 400. route names the surface for the message ("a single-record route", "the event stream"); alsoInclude adds ?include= to the rejected set (the event stream embeds nothing). Unknown top-level parameters keep their ADR-024 tolerance — only what the engine OWNS is checked.

func SingleParam

func SingleParam(params url.Values, key string) (string, bool, error)

SingleParam is singleValue for callers OUTSIDE this package that read an engine-owned parameter directly (codegen's ?include=): same ENG-17 contract — present-or-not plus a named rejection when repeated.

Types

type AggMetric

type AggMetric struct {
	Fn    string // sum | avg | min | max
	Field string
	Alias string // SQL alias, e.g. "agg_sum_salario"
}

AggMetric is one requested function over one field, with its SQL column alias.

type AggregateQuery

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

AggregateQuery is a validated aggregation over a resource, scoped by the SAME filters + RBAC row condition as a list read of that resource.

func BuildAggregate

func BuildAggregate(
	resource string,
	res *schema.ResourceSchema,
	params url.Values,
	condition *rbac.WhereCondition,
	allowedFields []string,
) (*AggregateQuery, error)

func (*AggregateQuery) GroupBy

func (aq *AggregateQuery) GroupBy() []string

GroupBy returns the group-by field names (empty = a single overall aggregate).

func (*AggregateQuery) HasCount

func (aq *AggregateQuery) HasCount() bool

HasCount reports whether COUNT(*) was requested.

func (*AggregateQuery) Metrics

func (aq *AggregateQuery) Metrics() []AggMetric

Metrics returns the requested sum/avg/min/max metrics (with their SQL aliases).

func (*AggregateQuery) SQL

func (aq *AggregateQuery) SQL() (sql string, args []any)

SQL emits the aggregate SELECT, scoped by the same WHERE (filters + RBAC row condition + search) the list read uses, minus cursor/pagination. All identifiers are quoted schema-validated names (injection-inert) and every function comes from the fixed allowlist. With group_by, rows are GROUP BY'd and ORDER BY'd on the group columns for deterministic output.

type IncludeError

type IncludeError struct {
	Status int
	Msg    string
}

IncludeError carries an HTTP status so the handler maps a bad/forbidden include to 400 / 403 (never a 500). A nil *IncludeError means success.

func BuildGetInclude

func BuildGetInclude(baseResource, include, baseSelect string, baseArgs []any, s *schema.APISchema, maxDepth int, rbacFor RelationRBAC) (sql string, args []any, ierr *IncludeError)

BuildGetInclude wraps a single-row base SELECT (SELECT * ... WHERE id=$1 [+ row cond]) so the result is one nested JSON object (column `data`), or zero rows (→ 404). baseSelect/baseArgs come from the caller's get-by-id SQL.

func BuildListInclude

func BuildListInclude(baseResource, include, baseSelect string, baseArgs []any, orderField, orderDir string, s *schema.APISchema, maxDepth int, rbacFor RelationRBAC) (sql string, args []any, ierr *IncludeError)

BuildListInclude wraps a base list SELECT so each row becomes a nested JSON object with its requested embeds, returning ONE query that yields two columns: `data` (a json array of the wrapped rows, ordered as the base query ordered them) and `n` (the row count, for has_next). baseSelect/baseArgs come from QueryBuilder.SQL(); orderField/orderDir from QueryBuilder.EffectiveOrder().

func (*IncludeError) Error

func (e *IncludeError) Error() string

type QueryBuilder

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

QueryBuilder holds a parsed, validated query ready to emit SQL.

func BuildQuery

func BuildQuery(
	resource string,
	res *schema.ResourceSchema,
	params url.Values,
	condition *rbac.WhereCondition,
) (*QueryBuilder, error)

BuildQuery parses url.Values and returns a validated QueryBuilder. Returns error for unknown filter fields, type-incompatible operators, or non-integer page/per_page.

func (*QueryBuilder) EffectiveOrder

func (qb *QueryBuilder) EffectiveOrder() (field, dir string)

EffectiveOrder returns the column and direction (ASC|DESC) that SQL() orders the base rows by — so the include path (RELATIONS-V1) can re-impose the SAME order on its json_agg of wrapped rows. It mirrors SQL()'s ORDER BY logic: keyset cursors order by id (DESC for ?before), an explicit ?sort wins otherwise, and the default is id ASC.

func (*QueryBuilder) Page

func (qb *QueryBuilder) Page() int

Page returns the current page number (1-based).

func (*QueryBuilder) PerPage

func (qb *QueryBuilder) PerPage() int

PerPage returns the current page size.

func (*QueryBuilder) SQL

func (qb *QueryBuilder) SQL() (selectQ, countQ string, selectArgs, countArgs []any)

SQL returns the SELECT and COUNT queries with their respective arg slices. selectArgs contains LIMIT/OFFSET appended after WHERE args; countArgs does not. When afterID or beforeID is set, keyset pagination is used: no OFFSET, one round-trip.

func (*QueryBuilder) UsesCursor

func (qb *QueryBuilder) UsesCursor() bool

UsesCursor reports whether this query paginates by keyset cursor (?after/ ?before) rather than by page/offset. The list handlers use it to shape meta: a cursor request has NO page number, so meta must not invent one (ENG-15 — meta.page used to echo the default or even a sent-and-ignored page, actively asserting a page the query never used).

type RelationRBAC

type RelationRBAC func(resource string) (allowed bool, fields []string, cond *rbac.WhereCondition)

RelationRBAC resolves, for a target resource, whether the request's role may read it, the field allowlist (empty = all fields), and the row-level condition to inject into the embed's WHERE. The caller wires this to policy.Evaluate so pkg/query stays decoupled from how RBAC is evaluated.

Jump to

Keyboard shortcuts

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