Bulk exports#
When an export means “everything that matches this filter” — not the one page of 50 rows the user is looking at — run the export through a dedicated handler instead of building the file in the browser. CsvHandler and ExcelHandler stream reactively, so the response starts flowing before the full result is in memory.
CSV with a curated column set#
@Component
@CrudMapping(value = "/v1/admin/user/export.csv", type = User.class)
public class UserCsvHandler implements CsvHandler, QueryHandler {
@Override
public List<ColumnMeta> getHeaders() {
return List.of(
ColumnMeta.of("loginName", "Email"),
ColumnMeta.of("firstName", "First Name"),
ColumnMeta.of("lastName", "Last Name"),
ColumnMeta.of("createdAt", "Created On")
);
}
@Override
public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
// honour whatever the request came with, but never export archived rows
filter.sqlExpression("status <> 'ARCHIVED'");
return filter;
}
}Excel with a read-side ACL#
@Component
@CrudMapping(value = "/v1/admin/user/export.xlsx", type = User.class)
public class UserExcelHandler implements ExcelHandler, QueryHandler {
@Autowired private AuthProvider auth;
@Override public String getReportName() { return "Users"; }
@Override
public int aclCheck(FilterCriteria criteria, HandlerContext ctx) {
return userProvider.hasRole("USER_EXPORT") ? 0 : -1;
}
@Override
public List<ColumnMeta> getHeaders() {
return UserCsvHandler.STANDARD_HEADERS; // share the header list
}
@Override
public QueryFilter applyQueryFilter(QueryFilter filter, HandlerContext ctx) {
filter.addCondition(new SimpleCondition("tenantId", userProvider.getTenantId()));
return filter;
}
}Frontend hookup#
Export endpoints are normal URLs — just point the browser at them:
<a href={`/api/v1/admin/user/export.csv?${queryString}`} download>
Export CSV
</a>For consistency with the rest of the wire layer, grab the URL from PalmyraGridStore.export which opens it in a new window — or call the endpoint directly with the same filter the grid is currently showing.
Guidelines#
- Share column sets. Put
getHeaders()output behind aSTANDARD_HEADERSconstant so CSV and Excel stay in lock-step. - Gate at
aclCheck. A download link is the least-guarded piece of UI on the page; the handler is the last place to enforce the permission. - Don’t compose with
CrudHandler. An export endpoint has a URL of its own — dedicated handler, dedicated route.
See also#
CsvHandler·ExcelHandler·CustomFormatHandlerfor formats beyond CSV/XLSX.ColumnMeta— whatgetHeaders()returns.