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 flaggedDropMode.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=trueResponse#
{
"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;
}
}