feat(api): add length validation to all *Filter query types (F9) #54

Merged
nrupard merged 2 commits from feat/pms-123-filter-validate into main 2026-06-04 01:00:37 +02:00
Owner

Closes PMS-123. Subtask of PMS-121 (F9).

Background

Most *Filter types in src/modules/*/models.rs already derive validator::Validate, but the derives were empty: no field-level constraints, so handlers calling filter.validate()? were passing untrusted query-string strings straight into SQL ILIKE / = patterns with no length cap.

Five filters were already fully validated (TicketFilter, CompanyFilter, ContactFilter, InvoiceFilter, PaymentFilter). Nine filters had the derive but no attributes. One filter (CalendarEventFilter in src/modules/calendar/mod.rs:41) lacked the derive entirely.

Change

  • AuditLogFilter: length(max = 100) on entity_type, action.
  • ContractFilter: length(max = 100) on contract_type, status.
  • KbArticleFilter: length(max = 100) on status, visibility; length(max = 200) on the free-text search field q.
  • AppointmentFilter: length(max = 100) on appointment_type.
  • TimeOffFilter: length(max = 100) on status.
  • ProjectFilter: length(max = 100) on status.
  • AssetFilter: length(max = 100) on status.
  • CalendarEventFilter: add the validator::Validate derive and wire filter.validate()? into the GET /api/v1/calendar/events handler (src/modules/calendar/routes.rs:65-70) so the route matches the convention even while it still returns an empty list.

The 100 / 200 split mirrors the existing TicketFilter / CompanyFilter / ContactFilter pattern (q at 200, tags at 500, short identifier-like fields at 100).

TimeEntryFilter and TimesheetFilter have no Option<String> fields, so the existing Validate derive plus the handler call are already sufficient.

Acceptance

  • Story PMS-121 AC "All *Filter query types validate input" is met after this PR.
  • just check-docker (release-profile builder stage) compiles cleanly.

Out of scope

  • The shared filter.validate()? helper called out in the parent story; the existing inline ? call site pattern is consistent across every list handler and works fine.
  • Renaming the env var on PMS-122 (CORS); that PR's separate.
Closes PMS-123. Subtask of PMS-121 (F9). ## Background Most `*Filter` types in `src/modules/*/models.rs` already derive `validator::Validate`, but the derives were empty: no field-level constraints, so handlers calling `filter.validate()?` were passing untrusted query-string strings straight into SQL `ILIKE` / `=` patterns with no length cap. Five filters were already fully validated (TicketFilter, CompanyFilter, ContactFilter, InvoiceFilter, PaymentFilter). Nine filters had the derive but no attributes. One filter (CalendarEventFilter in `src/modules/calendar/mod.rs:41`) lacked the derive entirely. ## Change - `AuditLogFilter`: `length(max = 100)` on `entity_type`, `action`. - `ContractFilter`: `length(max = 100)` on `contract_type`, `status`. - `KbArticleFilter`: `length(max = 100)` on `status`, `visibility`; `length(max = 200)` on the free-text search field `q`. - `AppointmentFilter`: `length(max = 100)` on `appointment_type`. - `TimeOffFilter`: `length(max = 100)` on `status`. - `ProjectFilter`: `length(max = 100)` on `status`. - `AssetFilter`: `length(max = 100)` on `status`. - `CalendarEventFilter`: add the `validator::Validate` derive and wire `filter.validate()?` into the `GET /api/v1/calendar/events` handler (`src/modules/calendar/routes.rs:65-70`) so the route matches the convention even while it still returns an empty list. The 100 / 200 split mirrors the existing TicketFilter / CompanyFilter / ContactFilter pattern (q at 200, tags at 500, short identifier-like fields at 100). `TimeEntryFilter` and `TimesheetFilter` have no `Option<String>` fields, so the existing `Validate` derive plus the handler call are already sufficient. ## Acceptance - Story PMS-121 AC "All `*Filter` query types validate input" is met after this PR. - `just check-docker` (release-profile builder stage) compiles cleanly. ## Out of scope - The shared `filter.validate()?` helper called out in the parent story; the existing inline `?` call site pattern is consistent across every list handler and works fine. - Renaming the env var on PMS-122 (CORS); that PR's separate.
feat(api): add length validation to all *Filter query types (F9)
All checks were successful
Check / fmt + clippy + compile + tests (pull_request) Successful in 27s
Build OCI container / Build and push mokosh-api image (push) Successful in 3m1s
b7c612febb
PMS-123. Most `*Filter` types already derive `validator::Validate`, but the derives were empty - no field-level constraints - so handlers calling `filter.validate()?` were passing untrusted query-string strings straight into SQL `ILIKE` / `=` patterns with no length cap.

Add `#[validate(length(max = ...))]` to every free-text field on the previously-uncapped filters: AuditLogFilter (entity_type, action), ContractFilter (contract_type, status), KbArticleFilter (status, visibility, q), AppointmentFilter (appointment_type), TimeOffFilter (status), ProjectFilter (status), AssetFilter (status). Cap is 100 for short enum-like identifiers, 200 for the free-text search field `q`. This matches the existing pattern on TicketFilter, CompanyFilter, ContactFilter, InvoiceFilter, and PaymentFilter (q at 200, tags at 500, status as typed enum where applicable).

CalendarEventFilter was the one filter type that did not derive `Validate` at all; add the derive and wire `filter.validate()?` into the `GET /api/v1/calendar/events` handler so it matches the convention even while the route still returns an empty list.

Filters whose only `Option<String>` field is already typed against an enum (CompanyType, CompanyStatus, BillingStatus, etc.) need no further constraints because the Deserialize step already rejects out-of-range values. TimeEntryFilter and TimesheetFilter have no `String` fields, so the existing `Validate` derive plus the handler call are sufficient.

Story-level F9 acceptance criterion "All *Filter query types validate input" is now met.

#PMS-123

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs(kb): note ILIKE cap rationale on KbArticleFilter.q
All checks were successful
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + compile + tests (pull_request) Successful in 4m16s
Build OCI container / Build and push mokosh-api image (push) Successful in 8m2s
7b67a7a50b
Matches the inline comment already present on TicketFilter.q so future readers see the same intent: the 200-char cap exists to keep ILIKE patterns sane, not to enforce a real product constraint.

#PMS-123

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/pms-123-filter-validate 2026-06-04 01:00:37 +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!54
No description provided.