What this is about
Own data and attribute filters cover the common cases. Sometimes a project needs a rule of its own, such as “only the editorial team sees drafts” or “archived contracts only with a certain attribute”. For that you write a custom data filter.
A custom data filter is a Spring bean that implements CdmsFilterInterface<T> for the DTO of a model. CDMS finds it by its type and asks it for a condition on every request on that model.
What a custom filter looks like
@Service
public class ContractArchiveFilter implements CdmsFilterInterface<ContractDto> {
@Override
public ListSearchFilter get() {
List<String> roles = RequestContextHolder.get().getEffectiveUserRoles();
if (roles != null && roles.contains("contract-archive"))
return null; // no restriction
return new ListSearchFilter("archived", "false", SearchLogicOperations.EQ);
}
}
You do not need more: no annotation for CDMS, no registration, no fixed class name. Only the type parameter matters, here ContractDto.
The filter runs once per request, not per row. So it has to express its rule as a condition the database can check. Field paths through relations are allowed, e.g. company.id.
What get() can return
When: get() returns a ListSearchFilter
CDMS adds the condition to the request as a mandatory filter, combined with AND.
Result: The person sees only rows that meet the condition.
When: get() returns null
CDMS adds nothing for this filter.
Result: No restriction by this filter. Other filters still apply.
When: get() throws an exception
The request fails with the status of the exception. The attribute filter does exactly this for a missing attribute: 422.
Result: Nothing is read or written.
The filter tree
CDMS builds every request on a model from an AND root. Your request and all security filters hang below it, side by side.
flowchart TB
W["AND"] --> C["client filters<br/>(from query)"]
W --> O["own data<br/>_userId = person"]
W --> A["attribute filter"]
W --> E1["custom filter 1"]
W --> E2["custom filter 2"]
If there are several custom filters for the same model, all of them apply at the same time. A row has to meet every condition.
Where the filter applies
| Request | Effect |
|---|---|
POST /query | Only matching rows; totalCount counts only them. |
POST /read/{id} | Non-matching row → 404. |
Lists and references in a response | Non-matching entries are missing; a non-matching single reference is null. |
PUT, PATCH, DELETE, rollback | CDMS first checks whether the row passes the filter; otherwise 404. |
So what you may not read, you may not change either. See Why invisible objects return 404.
When the filter does not fit
A filter that does not fit, for example with a typo in the field name, never silently drops out: without it more rows would come back, for a security filter every row. Who has to fix the error depends on where the filter comes from:
- unknown field or unsuitable operator → request rejected
- 400
unknown-search-key|…,unsupported-operator|… - the client corrects its request
- unknown field, unknown path step or unsuitable operator → request fails
- 500
unresolvable-mandatory-filter|<field>|… - better no result than too much
500 here means: the project is set up wrongly, not the request. The client cannot do anything about it. Check the field name in your filter against the model. The client can neither set nor switch off a mandatory filter.
Pitfalls
Where to go next
- The ready-made solution for profile attributes: Attribute filter
- Filters from the search point of view: Filters that always run along
- Where custom code belongs otherwise: Generated code and custom code