pub fn project_entity(
entity: &Value,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) -> ValueExpand description
Project an entity-shaped JSONB value into a GraphQL response object, mirroring the query path’s SQL projection exactly.
This is the single, canonical entity projector, shared by the mutation
success arm (projecting the returned entity) and the error arm (projecting the
error metadata). It is behaviourally equivalent to the SQL query projection
(projection_generator::render_field):
- output key = the selection’s response key (the camelCase GraphQL surface name, honouring aliases),
- source key =
to_snake_caseof the field name (the stored JSONB key), with acamelCasefallback for legacy metadata that used the surface casing, - object fields, single or listed, are projected through their sub-selection at any
depth. The recursion follows the selection, which
max_query_depthbounds, so there is no depth at which a stored sub-object passes through: that fall-through, at a cap of 4, served every stored key of an object five levels down, gated or not. An object field with no sub-selection selects none of its fields and projects to{}, - scalar fields pass through their stored value,
__typenameis emitted only where the client selected it.
type_name is the concrete GraphQL object type of entity (resolved by the
caller). selections is the result selection set for this level, with inline
... on T fragments preserved.
§An empty selection set projects nothing
It used to return the stored entity unchanged, and that one line is what made
#1352 and #1357 invisible: an empty slice is the permissive shape, not a
neutral one, so a transport that arrived without a selection set was not
refused — it was granted every field of the row, policy-gated ones included,
while selection_set_selects_gated_field reported that nothing gated was
selected and the #423 field authorizer took zero calls.
Nothing legitimate reaches here empty any more: GraphQL § 5.3.3 refuses a
composite field named without a selection set (#1357), every mutation’s return
type is composite (#1358), and the write entries take a non-empty
WriteSelections. An empty slice arriving
here is therefore a defect upstream, and the safe answer to a defect is the
empty object — which is also what the read path already answers for a selection
set that @skip resolves to nothing, so the two agree.
There is deliberately no project_entity_unfiltered to reach for. Every
caller in the tree either has a real selection set or guards emptiness before
calling (federation returns early on root.nested_fields.is_empty()), so a
named permissive helper would be a public function with no caller — and an
unused facility is how the permissive branch stayed reachable in the first
place. A caller that genuinely wants the whole entity can write entity.clone()
and own that decision where it is made.