How to Check if a Value is Empty in GraphQL
The following question was recently posted on GraphQL Discord's #tech-help channel:
Hey all. Can I return a boolean in place of an id? For example:
followed_by_pk(followed_id: $followedId, user_id: $userId) { result: !!id }
I just need a true or false to determine if there's a result
This is an interesting problem to solve, so in this article I'll explore what alternatives we have in GraphQL to tackle it.
What is the problem?
In the proposed example, users can follow, and be followed by, other users. As it was modeled in the database, the "user follows user" relationships are stored in a table, with each row containing three values:
- Relationship ID (which is the primary key)
- Followed user ID
- Following user ID
The root field followed_by_pk
receives the two user IDs, and finds if there is a relationship between them. If there is, this field returns an entity of type FollowedByRelationship
, containing the IDs as properties. If there is none, it returns null
:
type Query {
followed_by_pk(followed_id: ID!, user_id: ID!): FollowedByRelationship
}
type FollowedByRelationship {
id: ID!
followed: User!
following: User!
}
In order to explore the different solutions more thoroughly, I'll initially have field followed_by_pk
return the ID of the corresponding relationship (instead of an object), or null
if there is none:
type Query {
followed_by_pk(followed_id: ID!, user_id: ID!): ID
}
Now, the developer is only interested in finding out if the relationship exists or not, and has no use for the actual relationship ID. In other words, he expects the field to be resolved to a Boolean
with either values true
or false
, instead of an Int
or null
.
He asks if it's possible to prepend the field with !!
, as done in JavaScript to convert a value to boolean. But no, this is not possible, because GraphQL does not have a !
operator to prepend to fields (there is a proposal to append !
to the field, but it's for a different purpose).
Let's explore the different solutions.
Converting the type on the client
The first solution involves no changes on the GraphQL schema and query:
{
followed_by_pk(followed_id: $followedId, user_id: $userId) {
result: id
}
}
Then, we can use the value of type ID
to represent a Boolean
value:
- If an ID is returned, there is a relationship and the integer/string value represents
true
- Otherwise there is none and the
null
value representsfalse
However, this will produce an error in strongly typed clients (as when using TypeScript), since we cannot assign an integer or string value to a variable declared as boolean. To solve it, after executing the GraphQL query, the client will need to convert the result from Int
to Boolean
:
// `data.result` is int/string, `result` is boolean
const result = !! data.result;
This method is the simplest one, but has a few disadvantages:
- Extra logic is required in the client
- If multiple clients consume the API (possibly in different languages), this extra logic will be replicated across them, which avoids DRY (Don't Repeat Yourself) and increases the likelihood of bugs
- We can't directly use the types produced by codegen from the GraphQL schema
Duplicating the field
The next solution is not to use field followed_by_pk
, which returns an ID
, but a new field is_followed_by
which produces the response as a Boolean
:
type Query {
followed_by_pk(followed_id: ID!, user_id: ID!): ID
# Add a new field
is_followed_by(followed_id: ID!, user_id: ID!): Boolean!
}
This solution works perfectly. However, by adding yet another field, the GraphQL schema will become a bit less elegant, and we need to maintain extra code in the resolvers. If this duplication of fields happens repeatedly, we may eventually end up bloating the GraphQL schema.
Organizing the data under a new entity
With the previous solution, the root Query
type may end-up containing plenty of additional fields, becoming unruly. To avoid this, we can collect all related fields and place them as properties under a custom Payload
type, including field exists
of type Boolean
:
type Query {
followed_by(followed_id: ID!, user_id: ID!): FollowedByPayload!
}
type FollowedByPayload {
id: ID
exists: Boolean!
}
Please notice how this schema is slightly different to the original one:
- Original schema: field
followed_by_pk
, of typeFollowedByRelationship
, is nullable - This schema: field
followed_by
, of typeFollowedByPayload
, is not nullable
Because the field is non-null, we can always query its property exists
, which has the required type Boolean
, to find out if the relationship exists or not.
This method offers a neat way of adding as many properties as we need. For instance, we can also retrieve the User
entities, or the date when the relationship was created:
type FollowedByPayload {
id: ID
exists: Boolean!
# Can add yet more properties
followed: User
following: User
created: Date
}
This solution may be the most appropriate one, in that it allows to retrieve a property as ID
or Boolean
, which solves the challenge, while making the GraphQL schema a bit tidier.
Changing the type via a directive (don't do it!)
We might be tempted to use a query-type directive @isNotEmpty
, which would change the value of the field from the original ID to true
, and from null
to false
:
directive @isNotEmpty on FIELD
query {
followed_by_pk(followed_id: $followedId, user_id: $userId) {
result: id @isNotEmpty
}
}
The beauty of this directive is that it would work anywhere, on any field. Then, there would be no need to add multiple extra fields to the GraphQL schema whenever this problem arises; a single directive can take care of them all.
Unfortunately, having the directive change the type of the result will produce errors with strongly-typed clients, because field id
is expected to return a value of type ID
, not Boolean
.
Hence, we can't use this solution with strongly-typed clients. Indeed, this solution should be shunned upon: query-type directives modifying the type or value of the field in the response are not fully supported by the GraphQL spec, so use them carefully, and only if you really have to.
Nesting multiple queries
A logical solution to the problem of converting from ID
to Boolean
is to create a field isNotEmpty
that takes an ID
as input, and produces a Boolean
as output:
type Query {
isNotEmpty(id: ID): Boolean!
}
So now we have a new challenge to solve: how can we have the value of field followed_by_pk
be an input to field isNotEmpty
? When using StepZen, we can sequence a series of queries to produce a single result, via a custom directive @sequence
. Otherwise, there is not straightforward way in GraphQL to do this. (Some time ago I proposed the composable fields feature, but it has been rejected).
A generic way to solve it (which has been proposed for the spec) is by using an @export
directive, having a first query export the value to some dynamic variable, and a second query inject the dynamic variable into the target field:
query ExportResult {
followed_by_pk(followed_id: $followedId, user_id: $userId) {
id @export(as: "followedByPkID")
}
}
query ImportResult($followedByPkID: ID) {
result: isNotEmpty(id: $followedByPkID)
}
This solution works, but it just looks so bloated. Couldn't GraphQL natively offer a similar feature in a more elegant manner?
Using field references
Ok, there is no "field references" feature in the GraphQL spec, so right now this is science fiction. But since GraphQL could perfectly support it, I'll demonstrate how this hypothetical new feature could work.
Let's imagine that a field could reference the value of another field from the same entity, via a new syntax operator &
. For instance, in the query below, &me
references the value of field with alias me
, queried immediately above it:
{
me: loggedInUserID
user(id: &me) {
name
}
}
This feature would enable having the value of a field be input into another field. In the query above, if loggedInUserID
returns value 3
, then the subsequent field will be executing user(id: 3)
.
By repeating this operation multiple times, we can also chain field values to be used as inputs:
{
me: loggedInEmployeeID
myBoss: bossID(employeeID: &me)
user(id: &myBoss) {
name
}
}
For the sake of simplicity, the &
can only reference the value of a field queried on that same entity. However, if paired with the flat chain syntax proposal (that is, if it ever becomes accepted for the GraphQL spec) and adding a link back to the root Query
in the different types, we can then have entities reference values from other entities too:
{
posts {
generalDateFormat: query.settings.dateFormat
date(format: &generalDateFormat)
}
}
If "field references" ever became a reality, we could then provide field isNotEmpty
to convert field followed_by_pk
from ID
to Boolean
(as explained in the section above). This solution respects GraphQL's strong typing (unlike the @isNotEmpty
directive) and it's way simpler than exporting/importing values across queries (via @export
):
query {
followed_by_pk(followed_id: $followedId, user_id: $userId) {
id
result: isNotEmpty(&id)
}
}
Should GraphQL have this feature? Even though we can do the same via @export
, and the GraphQL spec favors no change, I think it could be justified. Should I submit a proposal for the spec, to have it debated there? If you think so, please send me a DM and let me know.
Conclusion
GraphQL is easy to use, but it could still be easier. As we've just seen in this article, for the simple task of converting a field value from integer to boolean, which in JavaScript can be achieved just by prepending !!
to the variable, we may need to do one of the following:
- Produce extra logic in the client
- Bloat the schema or the query
- Use directives that illegally modify the type
- Use a non-existing feature
Nevertheless, even if the chosen solution is not 100% ideal, it works. And as the GraphQL spec keeps being improved, these kinks are being ironed out.