Skip to content

eops-369-fix(commands): gate generic filter flags per resource - #20

Closed
mykhaylob-de wants to merge 1 commit into
mainfrom
eops-369-fix-generic-filter-gating
Closed

eops-369-fix(commands): gate generic filter flags per resource#20
mykhaylob-de wants to merge 1 commit into
mainfrom
eops-369-fix-generic-filter-gating

Conversation

@mykhaylob-de

@mykhaylob-de mykhaylob-de commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

_build_filter_params sent search, record_status, start_date and end_date to every resource. Public API filter schemas declare only their own fields, and django-ninja drops undeclared query params before validation, so unsupported flags came back as unfiltered results with exit code 0 — a wrong answer that looks right.

The audit named four resources. Cross-referencing every make_resource_app registration against the live /public/v2 OpenAPI schema found 13:

Resource Was silently dropped
depreciation-books --search, --status, --start-date, --end-date (all four)
classifications, companies, inbox, workflows, recurring/{invoices,bills,journal-entries} --status, --start-date, --end-date
customers, items, budgets, fixed-assets --start-date, --end-date
contracts --status

Two corrections to the ticket: record_status is declared on customers, items and budgets, so only the date flags were broken there; and their date filters do exist, under different names (updated_after/updated_before, from UpdatedAtRangeFilterMixin).

Changes

  • Each resource declares the filter flags its v2 list endpoint actually exposes. Flags outside that set are stripped from list, aliases included, so passing one exits non-zero naming the flag and the resource:

    $ dualentry customers list --start-date 2025-01-01
    Usage: dualentry customers list [OPTIONS]
    Error: customers does not support --start-date. Supported filters: --company, --search, --status.
    

    A genuine typo still gets click's own suggestion: --statNo such option: --stat (Possible options: --status).

  • filters is now a required keyword argument, so a new resource cannot inherit a wrong default.

  • contracts name their status filter status, not record_status. The CLI mapped --status to record_status, which the API ignored while the real filter stayed unreachable. Added status_param to the factory. fixed-assets declares both, and keeps the record_status mapping — that is what --status means everywhere else in this CLI.

  • Exposed --company/--customer/--vendor on the eleven resources that support them but were not offering them (journal-entries, fixed-assets, paper-checks, customers, vendors, contracts, budgets, workflows, bank-transfers, recurring/*).

On extra="forbid" (acceptance criterion 4)

Setting it on filter schemas would be a no-op, so it is not worth doing. django-ninja resolves query params through __ninja_flatten_map__, which copies only the schema's declared field names out of request.GET before pydantic runs. Extras never reach validation.

Verified empirically: a FilterSchema with model_config = ConfigDict(extra="forbid") still returns 200 and ignores ?bogus=1, and a model_validator(mode="before") shows pydantic receiving <DjangoGetter: {'name': 'a'}> with the extras already stripped. Independently confirmed against django-ninja's source (ninja/params/models.py:58-84, ninja/signature/details.py:148-218).

Rejecting unknown params server-side needs a different mechanism — middleware diffing request.GET against each operation's declared query params. That is separate work; this PR fixes the CLI only, so other v2 clients remain exposed.

Test plan

  • Unit tests pass (uv run pytest) — 212 passed, 125 skipped (skips are live-API tests gated on X_API_KEY).
  • Linter passes (uv run ruff check src/ tests/) — all checks pass; ruff format --check clean.
  • tests/test_filter_gating.py pins the full 35-resource flag matrix, covers customers/items/companies/budgets plus depreciation-books, classifications, workflows, inbox and recurring/*, exercises short aliases (-c, -s), and asserts no HTTP call is made when a flag is rejected.
  • Manually tested: customers list --start-date 2025-01-01 exits 2; customers list --status posted --company 7 sends {"record_status": "posted", "company_id": "7"}; contracts list --all --status active sends {"status": "active"} on the pagination path too.

@mykhaylob-de
mykhaylob-de force-pushed the eops-369-fix-generic-filter-gating branch from dffccd5 to 0fd007b Compare September 1, 2026 09:56
The list factory sent search, record_status, start_date and end_date to
every resource. Filter schemas in the public API declare only their own
fields and django-ninja drops the rest before validation, so unsupported
flags returned unfiltered results with exit code 0.

Each resource now declares the filter flags its /public/v2 list endpoint
actually exposes, verified against the v2 OpenAPI schema. Flags outside
that set are removed from the command, aliases included, so passing one
exits non-zero and names the flag and the resource. The argument is
required, so a new resource cannot inherit a wrong default.

Also:
- contracts name their status filter `status`, not `record_status`; the
  CLI mapped --status to record_status, which the API ignored while the
  real filter stayed unreachable. Fixed assets declare both and keep the
  record_status mapping, which is what --status means everywhere else.
- expose company/customer/vendor on the eleven resources that support
  them but were not offering them.
@mykhaylob-de
mykhaylob-de force-pushed the eops-369-fix-generic-filter-gating branch from 0fd007b to 7ed526a Compare September 1, 2026 09:56
@mykhaylob-de
mykhaylob-de deleted the eops-369-fix-generic-filter-gating branch September 1, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant