Why is it nullable?
It's common for people, particularly those using strongly typed GraphQL implementations such as ReasonML or TypeScript, to ask why certain elements in a PostGraphile schema are nullable. A lot of thought has gone into which parts should/should not be nullable, but the reasoning behind these decisions is not always obvious to users, so hopefully this article will help to explain.
Nulls in GraphQL
In GraphQL, nulls cascade up the tree until they find the first "nullable". So take for example this GraphQL Schema:
# This is a bad practice GraphQL schema to demonstrate a point.
type Numbers {
one: Int!
two: Int!
three: Int!
}
type Letters {
a: String!
b: String!
c: String!
}
type Query {
numbers: Numbers!
letters: Letters!
}
and this query:
{
numbers {
one
two
three
}
letters {
a
b
c
}
}
If there was an issue causing the resolver for Numbers.three
to throw an
error, then GraphQL would first try and make the field itself null. It can't,
because it's not nullable, so it would then try and make the parent field
(numbers
) null. But that's marked as non-nullable too, so the only thing left
to make nullable would be the entire query itself. This means that all the
letters, despite producing no errors, would also be omitted from the result.
One of the key aims of GraphQL is to deal smoothly with temporary errors - i.e. when an error occurs it aims to not "throw the baby out with the bathwater". This is one of the reasons (the main reason, really) that GraphQL treats all fields as nullable by default ("errors happen") and allows you to mark things as not null, rather than the other way around which is more common in typed languages. GraphQL wants you to think about where errors may occur and where they should be limited to, preventing them from flowing over into unrelated areas.
Root (Query, Mutation and Subscription) fields
If you're following GraphQL best practices, then all of your root level Query, Mutation and Subscription fields should be nullable unless you're absolutely certain that they cannot throw an error or be null, and further that none of their children or grandchildren or great-grandchildren can throw an error or return a null that would cascade and cause the field itself to be null.
In PostGraphile, two of our Query
fields are not nullable because they adhere
to this check:
nodeId
returns a set value (the string 'query') so it can never errorquery
returns theQuery
object again (it's a Relay 1 hack) and so it has all the same guarantees as the Query object
Everything else is nullable, because errors happen and we don't want them to cascade to sibling fields.
To make this even clearer: if our mutation fields were "not nullable" and you performed a mutation such as this:
mutation {
createSecret(input: {label: "Foo"}) { secret }
someOtherMutation(input: {...}) { ... }
}
If mutations were marked non-nullable and yet for some reason
someOtherMutation
threw an error, then the entire GraphQL response would come
back null and you wouldn't see the result of the createSecret
mutation. As per
the GraphQL spec: mutations are independent, thus the createSecret
mutation
would not be rolled back and the value would be created but never shown.
Relations: RLS visibility
PostgreSQL uses foreign keys to assert that relations exist. Take this SQL schema:
create table person (
id serial primary key,
username citext not null
);
create table post (
id serial primary key,
author_id int not null references person on delete cascade,
body text not null
);
From this we know that given a Post
record exists, then the associated
Person
object must also exist - PostgreSQL guarantees this. So why does
PostGraphile mark the Post.personByAuthorId
field as nullable? Well, consider
this:
-- Users can only see their own 'Person'
create policy select_self on person for select using (id = current_user_id());
-- Users can see all Posts
create policy select_all on post for select using (true);
Given the above, it's possible for you to be able to see a Post without you being allowed to see the associated Person. So even though the person definitely exists, that doesn't guarantee that you can see them.
Fields under mutation payloads
For similar reasons to the Relations above, it's possible for you to be able to create something but then not see the result of that - it really depends how you've defined your security. For example, if you create a truly anonymous "feedback" item then there's nothing in it to indicate that you're allowed to view it.
What about nullable nodes in table connections?
This one at first seems obviously a mistake - of course if I request a list of
rows from a table or function I'm not going to get some rows and some nulls -
they'll either all fail or all succeed... Surely? Well, it turns out: no -
functions which return connections (that is
CREATE FUNCTION ...(...) RETURNS SETOF table_type AS ...
) can return nulls as
well as table rows. In my opinion, doing so is a bad practice.
If you can commit to never returning null rows in your SETOF
functions, then
you can use the "no SETOF functions contain nulls" flag to change this
behaviour. I recommend this flag; but it's disabled by default to maximise
compatibility (also going from nullable to non-nullable is fine, but going the
other way is a breaking change).
-N, --no-setof-functions-contain-nulls
if none of your RETURNS SETOF compound_type functions mix NULLs with the results
then you may enable this to reduce the nullables in the GraphQL schema
What about computed fields?
It's very easy for computed fields (functions) to throw an error due to a logic issue in the function. We don't want that bringing down the entire schema so we leave these as nullable.
I'd be happy to accept a Pull Request that adds functionality marking a function
as non-nullable via a smart comment (e.g.
COMMENT ON FUNCTION foo_func(foo) IS E'@notNull';
) - do raise an issue if this
is of interest to you. Even with this, though, it would be unwise to mark
root-level functions as non-nullable - what if the PostgreSQL connection is
terminated when resolving that field, should that make all the other fields null
too? GraphQL best practices suggest that we should keep errors as localised as
we can.
I've read the above, but I still want this particular thing to be non-nullable!
Sure! PostGraphile is built with extensibility and customisability in mind - you can fix that with a plugin.
Here's a plugin which looks for all forward relation fields (like
personByAuthorId
) and changes their definition so that their type is the
GraphQLNonNull-wrapped version of their original type:
module.exports = function NonNullRelationsPlugin(builder) {
builder.hook("GraphQLObjectType:fields:field", (field, build, context) => {
if (
!context.scope.isPgForwardRelationField ||
!context.scope.pgFieldIntrospection?.keyAttributes?.every(
(attr) => attr.isNotNull,
)
) {
return field;
}
return {
...field,
type: new build.graphql.GraphQLNonNull(field.type),
};
});
};
If there's other things that are null but you think should not be, please raise and issue on GitHub and we'll either fix it, or update this document to explain why it's nullable.