Skip to content

feat(agent-bff): validate filters, sorts, and projections from capabilities#1755

Open
nbouliol wants to merge 8 commits into
feature/prd-674-expose-the-action-execute-endpoint-with-result-normalizationfrom
feature/prd-675-validate-filters-fields-sorts-and-projections-from
Open

feat(agent-bff): validate filters, sorts, and projections from capabilities#1755
nbouliol wants to merge 8 commits into
feature/prd-674-expose-the-action-execute-endpoint-with-result-normalizationfrom
feature/prd-675-validate-filters-fields-sorts-and-projections-from

Conversation

@nbouliol

@nbouliol nbouliol commented Jul 13, 2026

Copy link
Copy Markdown
Member

What

Adds a reusable, pure validator in agent-bff that checks a list/count request's filter, sort, and projection fields against the target collection's capabilities before any agent call, plus an operator normalizer. Capabilities are the only source of truth — no hardcoded type table is consulted, which fixes the drift between the frontend operator tables and what the agent actually supports.

This is the reusable validator only. Wiring it into the endpoints is out of scope (PRD-676/677).

How

  • operator-normalizer.ts — maps agent snake_case capabilities operators to the canonical PascalCase set. The snake→Pascal map is derived from datasource-toolkit allOperators using the same transform the agent uses to serialize them, so it round-trips every operator and cannot drift.
  • validation-errors.ts — three error factories owned by this slice, reusing the existing error-envelope shape ({ error: { type, status, message, details } }) without touching the shared registry.
  • capabilities-validator.tsvalidateAgainstCapabilities() returns every offending field as a BffHttpError[] (empty = valid); assertValidAgainstCapabilities() throws the first. Walks every filter leaf (including under And/Or groups).

Validation rules

Surface Rule Error
filter field absent from capabilities 422 unknown_field
filter field present with no operators 422 field_not_filterable
filter operator not in the field's normalized operators 400 invalid_filter_operator (+ validOperators)
sort field absent 422 unknown_field
projection field absent 422 unknown_field

Sort/projection check existence only; operator checks apply to filters. Each error carries the offending details.field.

Notable decisions

  • Normalize against the full allOperators set, not just uniqueOperators as the ticket wording suggests — real capabilities include in, contains, blank, etc.
  • A relation field appears in capabilities with no operators key (not operators: []); treated as field_not_filterable via operators ?? []. CapabilitiesResult.operators made optional to match runtime. See the ticket comment for the invariant discrepancy.
  • An unmapped capabilities operator throws 500 mapping_error (loud) rather than dropping or passing it through — this only happens on an agent/BFF version skew.

Tests

22 new focused unit tests: normalizer round-trip over all operators + edge cases; every error case; a fully-valid request; a mapping_error case; and an assertion that a field absent from capabilities is rejected with no type table consulted. Full suite: 660/660 green.

Fixes PRD-675

🤖 Generated with Claude Code

Note

Validate filters, sorts, and projections against collection capabilities in agent-bff

  • Adds validateAgainstCapabilities in capabilities-validator.ts to check request filter fields/operators, sort fields, and projection fields against collection capabilities, returning structured BffHttpError arrays.
  • Adds operator-normalizer.ts to convert between PascalCase and snake_case operator formats, bridging the agent's serialization format and the canonical @forestadmin/datasource-toolkit operator names.
  • Adds three typed error factories (unknownField, fieldNotFilterable, invalidFilterOperator) in validation-errors.ts with standardized HTTP status codes (400/422).
  • Risk: throws a 500 mapping_error if a capabilities operator cannot be normalized to a canonical name.

Macroscope summarized dc02e94.

nbouliol and others added 7 commits July 10, 2026 14:34
Add POST /v1/:collection/actions/:action/form: resolve the action via
the read-model action allow-list, load its form through agent-client
(tryToSetFields skips unknown submitted values), and return
{ fields, canExecute, requiredFields, skippedFields, layout }. No
activity log is written.

Extract shared token-guard/read-model/callAgent/decodeSegment helpers so
the new action middleware and the data middleware share one read-model
store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…vives

agent-client filters recordIds with .filter(Boolean) before serializing,
so a valid numeric primary key of 0 was silently dropped and the form
loaded as an unscoped/global action. Coerce ids to strings on the way in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry, read-model rethrow

- action-routes-middleware: non-POST and non-matching path fall through to next
- agent-action-client: real factory wires createRemoteAgentClient().collection().action()
- agent-route-helpers: resolveReadModel rethrows a non-schema error unchanged

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eld error

Carry the native action Error result's html on ActionFormValidationError and
throw a typed UnknownActionFieldError from the strict setFields, so consumers
can render error markup and route unknown fields to a client error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lization

Serve POST /v1/:collection/actions/:action/execute: reject unknown submitted
fields via setFields, run the action, and normalize the agent result into a
flat wrapper (success/error/webhook/redirect), returning 501 for File results.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validate value shapes (not just key presence) when normalizing the execute
result so malformed payloads fall through to 501 instead of a null-field 200,
and keep an empty roleIdsAllowedToApprove array in the approval 403 details.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lities

Add a reusable, pure validator that checks a request's filter, sort, and
projection fields against a collection's capabilities before any agent call,
plus an operator normalizer mapping agent snake_case operators to the canonical
PascalCase set from datasource-toolkit `allOperators`.

- unknown_field (422) for a filter/sort/projection field absent from capabilities
- field_not_filterable (422) for a filter field present with no operators
- invalid_filter_operator (400) with validOperators for an unsupported operator
- sort/projection check existence only; capabilities is the only source of truth
- unmapped capabilities operator throws 500 mapping_error (version skew)

`CapabilitiesResult.operators` made optional to match runtime (relations omit it).

Fixes PRD-675

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 13, 2026

Copy link
Copy Markdown

PRD-675

Comment thread packages/agent-bff/src/validation/capabilities-validator.ts Outdated
A condition-tree leaf always carries an operator; one without a valid
operator is malformed and must not reach the agent. Previously such a leaf
bypassed operator validation and was forwarded untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nbouliol
nbouliol force-pushed the feature/prd-674-expose-the-action-execute-endpoint-with-result-normalization branch from 911b16e to c4f48d5 Compare July 15, 2026 08:20
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