QueryHandler#

com.palmyralabs.palmyra.handlers.QueryHandler

Publishes paged, filtered reads.

Request — query parameters#

QueryHandler accepts the filter criteria as HTTP query parameters. Any parameter whose name matches a @PalmyraField.attribute is treated as an equality filter — ?status=ACTIVE&tenantId=3.

A handful of reserved parameters shape the page and the ordering:

Parameter Type Purpose
_limit number (default 15) Max number of rows returned
_offset number (default 0) Number of rows to skip — used for paging
_orderBy comma-separated fields Sort order — prefix a field with - for descending (e.g. _orderBy=-createdAt,name)
_total true / false When true, the framework runs an additional count query and populates total in the response

Response shape#

JSON envelope with the rows under result plus paging metadata:

{
  "result": [
    { "id": 1, "name": "...", "...": "..." },
    { "id": 2, "name": "...", "...": "..." }
  ],
  "limit":  15,
  "offset": 15,
  "total":  1923
}
  • result — an array of model objects. Each row carries every attribute declared on the model class from @CrudMapping.type, minus any field flagged DropMode.OUTGOING.
  • limit / offset — echoed back so the client can compute the next page.
  • total — only populated when the request supplied _total=true. Omitted otherwise (the extra count query is skipped for cheap scroll-style pagination).

Worked example — User#

Given this User model (shared across the mutating handler pages too):

@PalmyraType(type = "User", table = "users", preferredKey = "loginName")
public class User {
    @PalmyraField(primaryKey = true)                       private Long    id;
    @PalmyraField(sort = true, search = true,
                  mandatory = Mandatory.ALL)               private String  loginName;
    @PalmyraField(sort = true, search = true)              private String  firstName;
    @PalmyraField(sort = true, search = true)              private String  lastName;
    @PalmyraField(sort = true)                             private String  status;       // ACTIVE | ARCHIVED
    @PalmyraField(mandatory = Mandatory.SAVE)              private Long    tenantId;
    @PalmyraField(drop = DropMode.INCOMING)                private Instant createdAt;
    @PalmyraField(drop = DropMode.INCOMING)                private String  createdBy;
}

Request#

GET /api/v1/admin/user?status=ACTIVE&tenantId=3&_limit=15&_offset=0&_orderBy=-createdAt&_total=true

Response#

{
  "result": [
    {
      "id": 102,
      "loginName": "ada@example.com",
      "firstName": "Ada",
      "lastName":  "Lovelace",
      "status":    "ACTIVE",
      "tenantId":  3,
      "createdAt": "2026-04-01T09:12:00Z",
      "createdBy": "admin@example.com"
    },
    {
      "id": 101,
      "loginName": "grace@example.com",
      "firstName": "Grace",
      "lastName":  "Hopper",
      "status":    "ACTIVE",
      "tenantId":  3,
      "createdAt": "2026-03-28T14:05:00Z",
      "createdBy": "admin@example.com"
    }
  ],
  "limit":  15,
  "offset": 0,
  "total":  127
}

Methods#

Method Signature
aclCheck int aclCheck(FilterCriteria criteria, HandlerContext ctx)
applyQueryFilter QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx)
preProcess void preProcess(FilterCriteria criteria, HandlerContext ctx) — default copies ctx.getParams() into the criteria
onQueryResult Tuple onQueryResult(Tuple tuple, Action action)

Example#

@Component
@CrudMapping(value = "/v1/admin/user", type = User.class)
public class UserQueryHandler implements QueryHandler {

    @Autowired
    private UserProvider userProvider;

    @Override
    public int aclCheck(FilterCriteria criteria, HandlerContext ctx) {
        // 0 = allow, -1 = forbidden
        return userProvider.hasRole("USER_READ") ? 0 : -1;
    }

    @Override
    public void preProcess(FilterCriteria criteria, HandlerContext ctx) {
        // always apply the default behavior (copy params into criteria),
        // then layer additional, implicit criteria on top
        QueryHandler.super.preProcess(criteria, ctx);
        criteria.addAttribute("requestedBy", userProvider.getUserId());
    }

    @Override
    public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
        // inject tenant / soft-delete / visibility rules
        filter.addCondition("tenantId", userProvider.getTenantId());
        filter.addCondition("deleted", false);
        return filter;
    }

    @Override
    public Tuple onQueryResult(Tuple tuple, Action action) {
        // redact sensitive fields on every row
        tuple.remove("passwordHash");
        return tuple;
    }
}