Fields
Defining the fields that make up an object type, and where each field's value comes from.
A type's fields are defined by the fields function you pass when you implement it. That function receives a field builder (conventionally named t) and returns a map whose values each come from one method call on t.
Defining a field
Every field is created by a method on t. t.field is the base method the others are built on. It takes an options object; this example passes the field's type and a resolve function that produces the value:
faction: t.field({
type: Faction,
resolve: (character) => factions[character.factionId],
}),faction returns another object type, so its type is the Faction ref and the resolver looks the faction up by id. The resolve function's first argument is always the backing object; the Resolvers guide covers the rest of its signature and what a resolver may return. Every other field method on t is t.field with some of these options filled in for you.
Scalar fields
The scalar builders (t.string, t.int, t.float, t.boolean, and t.id) are t.field with the type already set to the matching scalar, so you supply only the resolve:
age: t.int({
resolve: (character) => REFERENCE_YEAR - character.birthYear,
}),age computes an Int from the character's birthYear, so t.int({ resolve }) produces the same field as t.field({ type: 'Int', resolve }) would. The other scalar builders work the same way for their types.
Exposing properties
When a field just returns a property already on the backing object, t.expose* writes that resolver for you: each helper is the matching scalar builder with a resolver that returns the named property, so you pass the property name instead of a resolve:
id: t.exposeID('id'),
name: t.exposeString('name'),
bio: t.exposeString('biography'),t.exposeID, t.exposeString, t.exposeInt, t.exposeFloat, and t.exposeBoolean each take the name of a property to read and produce a field of the matching scalar type. id and name read the properties of the same name from the Character object; bio reads the biography property, since the property name and the field name don't have to match. Each helper also has a …List form (t.exposeStringList and so on) for a property that holds an array.
Lists
A field's type can be a list by wrapping it in an array. In a resolver you return an array of the matching shape, the way the members field below returns a list of Character with t.field({ type: [Character] }). When the backing property already holds an array, the …List expose helpers forward it without a resolver:
titles: t.exposeStringList('titles'),titles exposes the string[] property as a [String] field, the list counterpart of t.exposeString.
Nullability
Every field defined so far is nullable. Pothos output fields are nullable by default, so t.exposeID('id') produces id: ID and any resolver may return null. Pass nullable: false to require a value:
id: t.exposeID('id', { nullable: false }),That makes the field id: ID!. A list field has two positions that can be null, the list itself and each of its items, so nullable also accepts a { list, items } object to set them independently:
titles: t.exposeStringList('titles', { nullable: { list: false, items: true } }),The default can also be flipped for the whole schema, so fields are non-nullable unless marked; Default nullability covers how.
Splitting fields across files
A type doesn't have to define all of its fields in one place. builder.objectField adds a single field to a type that's already been referenced, from anywhere in your codebase:
builder.objectField(Faction, 'members', (t) =>
t.field({
type: [Character],
resolve: (faction) =>
characters.filter((character) => character.factionId === faction.id),
}),
);This defines Faction.members in the module that owns characters, instead of alongside the rest of Faction. builder.objectFields adds several fields at once. The same pairing exists elsewhere: interfaceField and interfaceFields for interfaces, and queryField, mutationField, and their plural forms for the root types. These calls can run before or after the type is implemented, and Pothos merges the definitions when the schema builds. Project layout covers organizing a schema this way.