Skip to content

ADR-0045 — allowed.selectable takes root paths only

Status: accepted — restores ADR-0026 decision 4 to full force.

Context

allowed.selectable governs one thing only: which of this entity's own column names and declared computed-field names a request may name in select=. An included relation's projection is governed wholly by the target entity's own allowed.selectable (ADR-0026 decision 4) — a relation-dotted entry on the parent (selectable: ["id", "dictionary.id"]) is not a second, cross-entity meaning for the same key. To restrict what an included relation exposes, configure the target entity's own allowed.selectable, or don't make the relation includable at all.

Decision

allowed.selectable takes this entity's own column names and its declared computed-field names, and nothing else. A relation-dotted entry is a bootstrap ConfigurationException (KAVO_CONFIG_INVALID), in both the array and the { exclude } form.

  • allowed.selectable gets its own selector type, SelectableFieldSelector, capped to depth 1 (FieldPath with MaxDepth
    1. plus the entity's declared computed-field names — the same cap WritableFieldSelector already uses for creatable/updatable. A relation-dotted entry no longer type-checks, unlike on filterable/sortable, which keep QueryFieldSelector and still take one.
  • The runtime check in resolveAllowed stays, for an erased or cast config: any selectable entry that contains a . and is not itself a known field name is rejected, naming the entity, allowed.selectable, and the offending entry. (A genuine dotted column name — no adapter emits one today — is left alone; the rule stays precise.) This catches every ADR-0044 ceiling entry (dictionary.id), every relation-headed typo (notARelation.field), and the a.b.c deep form in one rule. The exception message tells the adopter to drop the entry or move the restriction to the target entity's own config.
  • ResolvedEntityConfig.relationProjection and its resolver (resolveRelationProjection) are removed. The resolved allowed.selectable is the configured array verbatim (no post-filter step), and projection equals it, as for any other explicit selectable.
  • DefaultIncludeResolver projects an included relation from the target entity's own resolved selectable only. With no select[<relation>]= in the request, the node carries no sparse fieldset and the target's default projection applies.
  • @kavo/nest Swagger synthesis drops the ceiling path entirely: every includable relation in a synthesized <Entity>Item/ListItem schema is emitted as the deferred x-kavo-includable-ref marker that registerKavoSchemas composes into a $ref to <Target>Item; the select[<relation>] query parameter carries no Restricted to: description.

Consequences

A relation-dotted selectable entry always throws at bootstrap. A config that names one must drop the entry, or restrict the relation's shape on the target entity's own allowed.selectable instead.

A parent cannot narrow an included relation from its own side. An included relation's projection is governed wholly by the target entity's own selectable (or its derived all-columns default). This is the accepted tradeoff — see Context. An unregistered relation target, which has no config to narrow, is served by its derived projection; there is no parent-side override for that case any more.

A new barrel type, SelectableFieldSelector. Added to the core barrel alongside QueryFieldSelector/WritableFieldSelector. QueryAllowed.selectable is retyped from QueryFieldSelector to it — a config that spelled a relation-dotted selectable entry now fails to compile as well as at bootstrap.

ResolvedEntityConfig loses a member. relationProjection is gone from the interface and from describeResolvedConfig's dump. Anyone hand-constructing a ResolvedEntityConfig through a cast (as the in-repo tests do) simply stops setting a key that no longer exists.

The synthesized Swagger <Entity>Item shape is simpler and more composed. Every includable relation now $refs <Target>Item (or degrades to { type: "object" } when the target publishes no synthesized item schema) — there is no longer an inline-object branch for a parent-ceilinged relation. select[<relation>] loses its per-entity Restricted to: description.

ADR-0026 decision 4 holds with no exception. A relation is projected by its own target's selectable, never the root's.

References

  • ADR-0026 (allowed.selectable narrows the response projection), whose decision 4 this restores.
  • ADR-0028 (includable relations live on allowed), for the relations.edges vs allowed split.
  • docs/internals/architecture/12-relations-and-includes.md §2 and §4; docs/internals/architecture/10-nestjs-integration.md; docs/features/allowed.md.