feat(reports): add safe custom report builder (PMS-180) #148

Merged
longjacksonle merged 1 commit from feat/pms-180-custom-report-builder into main 2026-06-09 23:02:22 +02:00

What

Add the safe custom report builder (PMS-180), the last open piece of PMS-93. PMS-93 rejected an ad-hoc SQL query builder for v1 (injection / performance risk); this is the curated, whitelisted alternative. The user never writes SQL or raw column names.

Design

src/modules/reports/custom.rs holds a server-side catalog of report sources (tickets, time_entries, assets), each exposing a whitelisted set of dimensions (group-by), measures (aggregates), and equality filters. The builder:

  • looks every dimension / measure / filter key up in the catalog; an unknown key is a 400 before any SQL is built (so an injection-style field name is just an unknown key),
  • builds the SELECT from &'static str expressions only,
  • binds every user value and the date range,
  • always injects tenant_id,
  • clamps limit (default 100, max 1000), requires at least one measure, and only ever generates SELECT.

Results come back as the generic columns / rows / totals envelope (the shape PMS-93 AC2 wanted), with an optional "format": "csv".

Endpoints (both RequireReports)

  • GET /api/v1/reports/custom/schema - the catalog, so the SPA renders the builder dynamically.
  • POST /api/v1/reports/custom - validate a spec and run it.

A custom descriptor is added to the GET /reports registry for discoverability.

Tests

tests/reports.rs gains custom_report_runs_and_exports, custom_report_rejects_unknown_fields (unknown / malicious source, dimension, measure, and empty-measure all 400; a valid spec succeeds), and custom_report_is_tenant_scoped. All 8 reports tests pass; 115 lib tests, clippy --all-targets -- -D warnings, and cargo fmt --check are clean.

Out of scope (future)

Saved / shareable report definitions, scheduled delivery, charts on custom results, cross-source joins, non-equality operators, and financial sources behind the manager gate.

🤖 Generated with Claude Code

## What Add the safe **custom report builder** (PMS-180), the last open piece of PMS-93. PMS-93 rejected an ad-hoc SQL query builder for v1 (injection / performance risk); this is the curated, whitelisted alternative. The user never writes SQL or raw column names. ## Design `src/modules/reports/custom.rs` holds a server-side **catalog** of report sources (**tickets**, **time_entries**, **assets**), each exposing a whitelisted set of dimensions (group-by), measures (aggregates), and equality filters. The builder: - looks every dimension / measure / filter key up in the catalog; an unknown key is a **400** before any SQL is built (so an injection-style field name is just an unknown key), - builds the SELECT from `&'static str` expressions only, - **binds** every user value and the date range, - always injects `tenant_id`, - clamps `limit` (default 100, max 1000), requires at least one measure, and only ever generates `SELECT`. Results come back as the generic **columns / rows / totals** envelope (the shape PMS-93 AC2 wanted), with an optional `"format": "csv"`. ## Endpoints (both `RequireReports`) - `GET /api/v1/reports/custom/schema` - the catalog, so the SPA renders the builder dynamically. - `POST /api/v1/reports/custom` - validate a spec and run it. A `custom` descriptor is added to the `GET /reports` registry for discoverability. ## Tests `tests/reports.rs` gains `custom_report_runs_and_exports`, `custom_report_rejects_unknown_fields` (unknown / malicious source, dimension, measure, and empty-measure all 400; a valid spec succeeds), and `custom_report_is_tenant_scoped`. All 8 reports tests pass; 115 lib tests, `clippy --all-targets -- -D warnings`, and `cargo fmt --check` are clean. ## Out of scope (future) Saved / shareable report definitions, scheduled delivery, charts on custom results, cross-source joins, non-equality operators, and financial sources behind the manager gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(reports): add safe custom report builder (PMS-180)
All checks were successful
E2E (staging) / Playwright against staging (pull_request) Successful in 57s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + compile + tests (pull_request) Successful in 2m26s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m25s
cc0023b782
PMS-93 rejected an ad-hoc SQL query builder for v1 (injection / performance risk). This adds the safe alternative: a curated, whitelisted builder where the user never writes SQL or raw column names.

The new src/modules/reports/custom.rs holds a server-side catalog of report sources (tickets, time_entries, assets), each exposing a whitelisted set of dimensions (group-by), measures (aggregates), and equality filters. Every identifier in the generated SQL comes from a &'static str constant; every user-supplied value is a bound parameter; tenant_id is always injected; only SELECT is ever generated. Unknown source / dimension / measure / filter keys are rejected with 400 before any SQL is built, so an injection-style field name is simply an unknown key. Results come back as the generic columns / rows / totals envelope (the shape PMS-93 AC2 wanted), with an optional CSV format.

Endpoints (both guarded by RequireReports): GET /api/v1/reports/custom/schema returns the catalog so the SPA can render the builder dynamically; POST /api/v1/reports/custom validates a spec and runs it. The limit is clamped (default 100, max 1000), at least one measure is required, and the date range is an inclusive from/to on the source's date column. A custom descriptor is added to the GET /reports registry for discoverability.

Tests in tests/reports.rs: custom_report_runs_and_exports (schema advertises the source; a grouped aggregate returns the right columns / rows / totals; CSV export works), custom_report_rejects_unknown_fields (unknown / malicious source, dimension, measure, and empty-measure specs all 400, while a valid spec succeeds), and custom_report_is_tenant_scoped (a second tenant's rows never reach this tenant's counts). All 8 reports tests pass; 115 lib tests, clippy --all-targets, and fmt are clean.

Out of scope (future): saved / shareable report definitions, scheduled delivery, charts on custom results, cross-source joins, non-equality operators, and financial sources behind the manager gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch feat/pms-180-custom-report-builder 2026-06-09 23:02:22 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!148
No description provided.