Custom query filters#
When your read endpoint needs to enforce implicit conditions — a tenant scope, a soft-delete flag, a visibility rule — don’t require clients to pass them. Override applyQueryFilter on the handler and append the conditions server-side.
The hook#
@Override
public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
// conditions the caller cannot override
filter.sqlExpression("status <> 'ARCHIVED'");
filter.addCondition(new SimpleCondition("tenantId", userProvider.getTenantId()));
// default ordering if the caller didn't ask
if (!filter.hasOrderBy()) {
filter.addOrderDesc("createdAt");
}
// hard cap on page size
if (filter.getLimit() == 0 || filter.getLimit() > 500) {
filter.setLimit(500);
}
return filter;
}Patterns worth reusing#
- Tenant scoping. Read the tenant from
AuthProvider(or a customUserProvider) and add it as a condition; never trust a tenant id from the request. - Soft delete. Use
sqlExpression("status <> 'ARCHIVED'")on the default path; publish a sibling handler at/admin/...without the clause for operators who need to see archived rows. - Role-based projection. Call
filter.setFields(...)fromapplyQueryFilterto strip columns a given role shouldn’t see (PII, financial totals). - Default ordering. Only apply a default when the caller didn’t supply one —
filter.hasOrderBy()is the guard.
Filtering by a parent-table attribute#
addCondition accepts dotted attribute paths, so you can filter on a column that lives on a joined parent table. Palmyra auto-includes the JOIN — you never write it in Java.
Imagine a UserModel with a foreign key to department:
@PalmyraType(type = "User")
public class UserModel {
@PalmyraField(primaryKey = true) private Long id;
@PalmyraField private String loginName;
@PalmyraField(attribute = "department")
private DepartmentModel department;
}To restrict the query to users in the HR department, add a condition on department.code:
@Override
public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
filter.addCondition(new SimpleCondition("department.code", "hr"));
return filter;
}At query time the framework expands this into a SELECT that joins users → department and appends the WHERE department.code = 'hr' predicate — no hand-written SQL, no custom addlJoin, no JPQL. The same rule applies to multi-hop paths: "department.manager.loginName" walks user → department → manager → column and adds every JOIN in the chain.
Use the same pattern when the condition comes from the caller rather than the handler — inside preProcess you can pull a raw param out of FilterCriteria and promote it into a dotted-path condition before the framework compiles the SQL.
See also#
QueryHandler— full method table.QueryFilter— every builder method (sqlExpression,addCondition,setFields,addOrderAsc/Desc, paging).FilterCriteria— the client-supplied side that compiles intoQueryFilter.