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 custom UserProvider) 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(...) from applyQueryFilter to 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 into QueryFilter.