Fields for Object and Interface types are defined using a shape
function. This is a function that takes a FieldBuilder as an argument, and
returns an object who's keys are field names, and who's values are fields created by the
FieldBuilder. These examples will mostly add fields to the Query
type, but
the topics covered in this guide should apply to any object or interface type.
Scalar fields can be defined a couple of different ways:
builder.queryType({
fields: t => ({
name: t.field({
description: 'Name field'
type: 'String',
resolve: () => 'Gina',
}),
}),
});
Convenience methods are just a wrapper around the field
method, that omit the type
option.
builder.queryType({
fields: (t) => ({
id: t.id({ resolve: () => '123' }),
int: t.int({ resolve: () => 123 }),
float: t.float({ resolve: () => 1.23 }),
boolean: t.boolean({ resolve: () => false }),
string: t.string({ resolve: () => 'abc' }),
idList: t.idList({ resolve: () => ['123'] }),
intList: t.intList({ resolve: () => [123] }),
floatList: t.floatList({ resolve: () => [1.23] }),
booleanList: t.booleanList({ resolve: () => [false] }),
stringList: t.stringList({ resolve: () => ['abc'] }),
}),
});
Fields for non-scalar fields can also be created with the field
method.
Some types like Objects and Interfaces can be referenced by name if they have a backing model defined in the schema builder.
const builder = new SchemaBuilder<{
Objects: { Giraffe: { name: string } };
}>({});
builder.queryType({
fields: t => ({
giraffe: t.field({
description: 'A giraffe'
type: 'Giraffe',
resolve: () => ({ name: 'Gina' }),
}),:
}),
});
For types not described in the SchemaTypes
type provided to the builder, including types that can
not be added there like Unions and Enums, you can use a Ref
returned by the
builder method that created them in the type
parameter. For types created using a class
(Objects or Interfaces) or Enums created using a typescript
enum, you can also use the the class
or enum
that was used to define them.
const LengthUnit = builder.enumType('LengthUnit', {
values: { Feet: {}, Meters: {} },
});
builder.objectType('Giraffe', {
fields: (t) => ({
preferredNeckLengthUnit: t.field({
type: LengthUnit,
resolve: () => 'Feet',
}),
}),
});
builder.queryType({
fields: (t) => ({
giraffe: t.field({
type: 'Giraffe',
resolve: () => ({ name: 'Gina' }),
}),
}),
});
To create a list field, you can wrap the the type in an array
builder.queryType({
fields: t => ({
giraffes: t.field({
description: 'multiple giraffes'
type: ['Giraffe'],
resolve: () => [{ name: 'Gina' }, { name: 'James' }],
}),
giraffeNames: t.field({
type: ['String'],
resolve: () => ['Gina', 'James'],
})
}),
});
Unlike some other GraphQL implementations, fields in Pothos are non-nullable by default. It is still often desirable to make fields in your schema nullable. This default can be changed in the SchemaBuilder constructor, see Changing Default Nullability.
builder.queryType({
fields: (t) => ({
nullableField: t.field({
type: 'String',
nullable: true,
resolve: () => null,
}),
nullableString: t.string({
nullable: true,
resolve: () => null,
}),
nullableList: t.field({
type: ['String'],
nullable: true,
resolve: () => null,
}),
spareseList: t.field({
type: ['String'],
nullable: {
list: false,
items: true,
},
resolve: () => [null],
}),
}),
});
Note that by default even if a list field is nullable, the items in that list are not. The last example above shows how you can make list items nullable.
Some GraphQL implementations have a concept of "default resolvers" that can automatically resolve field that have a property of the same name in the underlying data. In Pothos, these relationships need to be explicitly defined, but there are helper methods that make exposing fields easier.
These helpers are not available for root types (Query, Mutation and Subscription), but will work on any other object type or interface.
const builder = new SchemaBuilder<{
Objects: { Giraffe: { name: string } };
}>({});
builder.objectType('Giraffe', {
fields: (t) => ({
name: t.exposeString('name', {}),
}),
});
The available expose helpers are:
exposeString
exposeInt
exposeFloat
exposeBoolean
exposeID
exposeStringList
exposeIntList
exposeFloatList
exposeBooleanList
exposeIDList
Arguments for a field can defined in the options for a field:
builder.queryType({
fields: (t) => ({
giraffeByName: t.field({
type: 'Giraffe',
args: {
name: t.arg.string({ required: true }),
},
resolve: (root, args) => {
if (args.name !== 'Gina') {
throw new NotFoundError(`Unknown Giraffe ${name}`);
}
return { name: 'Gina' };
},
}),
}),
});
For more information see the Arguments Guide.
In addition to being able to define fields when defining types, you can also add additional fields independently. This is useful for breaking up types with a lot of fields into multiple files, or co-locating fields with their type (eg. add all query/mutation fields for a type in the same file where the type is defined).
builder.queryFields((t) => ({
giraffe: t.field({
type: Giraffe,
resolve: () => new Giraffe('James', new Date(Date.UTC(2012, 11, 12)), 5.2),
}),
}));
builder.objectField(Giraffe, 'ageInDogYears', (t) =>
t.int({
resolve: (parent) => parent.age * 7,
}),
);
To see all they methods available for defining fields see the SchemaBuilder API
You can use t.listRef
to create a list of lists
const Query = builder.queryType({
fields: (t) => ({
example: t.field({
type: t.listRef(
t.listRef('String'),
// items are non-nullable by default, this can be overridden
// by passing `nullable: true`
{ nullable: true },
),
resolve: (parent, args) => {
return [['a', 'b'], ['c', 'd'], null];
},
}),
}),
});