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.selectablegets its own selector type,SelectableFieldSelector, capped to depth 1 (FieldPathwithMaxDepth- plus the entity's declared computed-field names — the same cap
WritableFieldSelectoralready uses forcreatable/updatable. A relation-dotted entry no longer type-checks, unlike onfilterable/sortable, which keepQueryFieldSelectorand still take one.
- plus the entity's declared computed-field names — the same cap
- The runtime check in
resolveAllowedstays, for an erased or cast config: anyselectableentry 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 thea.b.cdeep 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.relationProjectionand its resolver (resolveRelationProjection) are removed. The resolvedallowed.selectableis the configured array verbatim (no post-filter step), andprojectionequals it, as for any other explicitselectable.DefaultIncludeResolverprojects an included relation from the target entity's own resolvedselectableonly. With noselect[<relation>]=in the request, the node carries no sparse fieldset and the target's default projection applies.@kavo/nestSwagger synthesis drops the ceiling path entirely: every includable relation in a synthesized<Entity>Item/ListItemschema is emitted as the deferredx-kavo-includable-refmarker thatregisterKavoSchemascomposes into a$refto<Target>Item; theselect[<relation>]query parameter carries noRestricted 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.selectablenarrows the response projection), whose decision 4 this restores. - ADR-0028 (includable relations live on
allowed), for therelations.edgesvsallowedsplit. docs/internals/architecture/12-relations-and-includes.md§2 and §4;docs/internals/architecture/10-nestjs-integration.md;docs/features/allowed.md.