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 a STANDARD_HEADERS constant 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#