Fields
Select which resource fields are returned — or extend/shrink the server's default selection.
| URL key | fields |
| AST nodes | Fields / Field { name, operator?, condition? } |
| Schema options | allowed, default, mapping, validate / validateMany |
On the wire
fields=id,name,email one list for the root resource
fields[items]=id,name per-relation lists
fields=+email extend the default selection
fields=-name shrink itThe equivalent parser input shapes (what SimpleParser and the URL decoder accept):
{ fields: 'id,name,email' } // comma-separated string
{ fields: ['id', 'name'] } // array of names
{ fields: { items: ['id', 'name'] } } // record keyed by relation
{ fields: [['id'], { items: ['id'] }] } // tuple: base + relationsInclude & exclude modifiers
A field name can carry a prefix that changes how it combines with the schema's default selection:
| Syntax | Meaning |
|---|---|
name | Select this field (replaces the default selection). |
+name | Include — extends the default selection instead of replacing it. |
-name | Exclude — removes the field from the selection. |
// schema default is ['id', 'name'] —
{ fields: '+email' } // → id, name, email
{ fields: '-name' } // → idIn the AST, the prefix becomes Field.operator (FieldOperator.INCLUDE / FieldOperator.EXCLUDE).
Building in code
The same shapes work as typed build input — field paths checked against the record type:
defineQuery<User>({ fields: ['id', '+email'] });
defineFields<User>(['id', 'name']); // standalone fragmentFields of related records
Fields of a relation use the relation name as key (or a relation.field path) and validate against the related schema, resolved through schemaMapping. The relation itself must be allowed and requested via relations.
{
relations: ['items'],
fields: { items: ['id', 'name'] },
}A per-relation fieldset governs the projection of an included relation: the backends hydrate items with exactly the listed columns (@rapiq/adapter-typeorm additionally selects the relation's primary key, so the relation object hydrates reliably even when every listed column is NULL). When the related schema declares fields.default (or an allowed list), those apply to the include the same way. Only an include without any fieldset, against a schema without a fields block, hydrates the whole record.
Schema options
defineSchema<User>({
fields: {
allowed: ['id', 'name', 'email', 'age'],
default: ['id', 'name'],
mapping: { displayName: 'name' },
},
});| Option | Description |
|---|---|
allowed | Selectable field names. Omit to allow all; [] blocks the parameter. |
default | Selection when the client sends nothing (or only +/- modifiers). |
mapping | Alias → field translation applied before validation. |
validate / validateMany | Per-request key hooks: accept, reject, or gate a field per actor. |
Row-scoped fields
A validate / validateMany hook may answer with a condition instead of a boolean. The field stays selected, but the condition lands on the Field node as Field.condition and marks the column visible only on rows satisfying it:
import { eq } from '@rapiq/core';
defineSchema<User, Actor>({
name: 'user',
fields: {
allowed: ['id', 'name', 'salary'],
validate: (field, actor) => field !== 'salary' ||
eq('realm_id', actor.realmId),
},
});The gate never removes a row; it only blanks a column on the rows that fail it. @rapiq/adapter-memory applies it while projecting; the SQL backends project the column and rely on a post-fetch pass. Details and the fail-open caveat: Condition verdicts.
A gate also resists composition: a merge collision that would replace a gated field with an ungated one (or with a different gate) throws a MergeError (ErrorCode.FIELDS_CONDITION_DISCARDED) instead of silently widening disclosure.
On violation
Disallowed or invalid field input is dropped silently; with throwOnFailure it throws a FieldsParseError instead.