Browse all documentation

Filters and paths

Build Go filters with query paths, comparison values, and conditions, then use them in reads and access rules.

Use the query package to describe which documents you want. For example, “products costing at most 50 that are in stock” becomes a filter you can pass to the local API. The same package also builds filters for access rules.

Three types appear together:

Type What it means Example
query.Path Which field to check The product’s price
query.Value What to compare it with The number 50
query.Expression The resulting condition price is at most 50

Building an expression does not fetch any documents. The local API executes it when you make a read or other operation. For REST and TypeScript filter syntax, sorting, and pagination, see Querying data.

Build a filter in Go

This helper builds a filter for products in stock and within a budget. Put it in content/product_filters.go:

content/product_filters.go
package content

import "github.com/riducms/ridu/query"

func AffordableProducts(maxPrice float64) (query.Expression, error) {
  price, err := query.NewPath("price")
  if err != nil {
    return nil, err
  }
  inStock, err := query.NewPath("inStock")
  if err != nil {
    return nil, err
  }

  // Describe both requirements; this does not read the database yet.
  return query.And(
    query.LessThanEqual(price, query.Number(maxPrice)),
    query.Equal(inStock, query.Boolean(true)),
  )
}

NewPath names each field. query.Number(maxPrice) and query.Boolean(true) give the comparisons their values. query.And joins the two conditions: a document must satisfy both.

The helper returns a query.Expression. Keep it in a variable, pass it to another function, or reuse it in several requests. Expressions do not change after construction.

Choose a field with a path

A query.Path represents one field location. It is a validated Go value, so functions accepting a path can rely on its spelling being valid. A plain string has not passed that check.

To name Write Resulting path
A top-level title query.NewPath("title") title
A title inside the SEO group query.NewPath("seo", "title") seo.title
A dotted path already held as text query.ParsePath("seo.title") seo.title

NewPath takes separate segments. query.NewPath("seo.title") returns an error because the dot is not part of a field name. Use ParsePath for a dotted string, such as one received from a filter form. Both functions return errors for malformed paths, including empty segments; handle those errors before building the comparison.

This first check does not know your schema. query.NewPath("unknown") succeeds because the name is well formed, but a request using it fails if the collection has no such field. Ridu checks the field and its permissions when the operation runs. Fields with read-access rules also restrict caller-supplied filters; see protected field queries.

Reach groups, arrays, and blocks

Use authored field names, with a block’s configured key where needed:

Structure Path What it checks
seo group seo.title The group’s title
variants array variants.sku SKU values across the product’s rows
layout blocks with a hero block layout.hero.heading Headings in Hero rows only

An array path contains no row index or row key. Equality on variants.sku matches a document if any row has the requested SKU. The hero segment selects the configured block type; leaving it out of layout.hero.heading produces an invalid path.

Two comparisons joined with And are checked independently. A product with a red variant costing 30 and a blue variant costing 5 can match both “SKU equals red” and “variant price is at most 10”. Those conditions do not require the same row to match. There is currently no same-row grouping operator or all-rows operator.

For a repeated path, NotEqual excludes a document when any row equals the value. A product with both red and blue variants therefore does not match “SKU is not red”.

Use values that match the field

query.Value is the value inside a comparison. Construct it with the Go helper for the value you mean:

Value to compare with Constructor
Text or a relationship ID query.String("notebook")
A price or other number query.Number(50)
A checkbox value query.Boolean(true)
Null query.Null()
Several candidates for a membership check query.In(path, query.String("red"), query.String("blue"))

query.String("50") is text, while query.Number(50) is a number. Use the kind that matches the field. query.In accepts candidate values and builds its list; query.List(...) is also available when constructing a comparison through query.Compare.

The similar names in store serve a different purpose. store.Number(50) is a document value used when creating or reading content. query.Number(50) is a comparison value used when finding content. They are separate Go types and cannot be substituted for each other. See Documents and values for working with document data.

Combine conditions and handle errors

Use query.And when all conditions must pass, query.Or when any condition may pass, and query.Not to negate a condition. And and Or require at least two expressions. Passing one, none, or a nil child returns an error. If an optional filter is your only condition, use that expression directly.

For comparisons you control in code, helpers such as Equal, LessThanEqual, and Contains keep expressions short. They expect a valid path and compatible comparison value; invalid arguments can panic. In the example, NewPath is checked first and LessThanEqual receives a number.

When the operator or value comes from input, use query.Compare, which returns an error for invalid combinations. For example, OperatorExists expects query.Boolean(...), while OperatorIn expects a list. The operator table explains the available comparisons and text matching.

Use a filter as a permission rule

An access rule returns a decision about what the caller may do. Wrap an expression in ridu.Where to allow access only to matching documents.

This collection allows reads only when its Visible checkbox is selected:

content/products.go
package content

import (
  "github.com/riducms/ridu"
  "github.com/riducms/ridu/field"
  "github.com/riducms/ridu/query"
)

func visibleProducts(ridu.AccessContext) (ridu.AccessDecision, error) {
  visible, err := query.NewPath("visible")
  if err != nil {
    return ridu.Deny(), err
  }
  // This filter limits every read, including reads with other filters.
  return ridu.Where(
    query.Equal(visible, query.Boolean(true)),
  ), nil
}

var Products = ridu.Collection{
  Slug: "products",
  Fields: field.Fields{
    field.Text("title").Required(),
    field.Number("price").Required().Min(0),
    field.Checkbox("inStock").Default(false),
    field.Checkbox("visible").Default(false),
  },
  Access: ridu.CollectionAccess{Read: visibleProducts},
}

Add Products to Config.Collections. The read rule applies to reads from the admin, REST, the SDK, and the local API. Configure create, update, and delete access separately for your application.

Execute a filtered read

Pass the expression directly to ridu.ListOptions.Where. This helper combines the budget filter with the collection’s visibility rule:

content/find_products.go
package content

import (
  "context"

  "github.com/riducms/ridu"
  "github.com/riducms/ridu/store"
)

func FindAffordableProducts(
  ctx context.Context,
  local *ridu.LocalAPI,
  maxPrice float64,
) (store.Page, error) {
  filter, err := AffordableProducts(maxPrice)
  if err != nil {
    return store.Page{}, err
  }

  // List executes the filter and also applies collection read access.
  return local.List(ctx, "products", ridu.ListOptions{
    Where: filter,
    Page:  1,
    Limit: 20,
  })
}

Call FindAffordableProducts(ctx, app.Local(), 50). A visible, in-stock product costing 40 is included. A hidden product, an out-of-stock product, or one costing 70 is excluded. Both the caller filter and the access filter are applied before pagination.

The two uses of “where” have different jobs:

  • ridu.ListOptions{Where: filter} asks for matching documents and still obeys access rules.
  • ridu.Where(filter) is the decision returned by an access rule.

The example makes an anonymous read because it supplies no actor. For a read on behalf of a signed-in user, also set Actor and ActorCollection in the list options. See passing the caller.

Use the same filter with generated collections

The dynamic API above returns store.Page, whose documents hold store.Values. Generated Go collection handles return your application’s document types. For a generated ProductsCollection handle, call generated.ProductsCollection.With(app.Local()).List(...) with ridu.TypedListOptions; its Where property takes the same query.Expression.

Generation gives Go callers typed document models; you still build filters with this query package. The generated TypeScript SDK instead accepts typed where objects, as shown in Querying data.

Keep ordering and page size in the request

A filter chooses which documents match. Page, Limit, and Sort belong to the list options. For Go sorting, build a term with query.NewSort(path, query.Ascending) or query.Descending and put it in the options’ Sort list. A field can be filterable without being sortable; repeated array paths are one example.

See sorting and pagination for request limits, ordering, and result metadata. The Local Go API covers the surrounding read options, while the query reference lists each constructor and its arguments.