I have a tree data structure that I would like to return via a GraphQL API.
The structure is not particularly large (small enough not to be a problem to return it in one call).
The maximum depth of the structure is not set.
I have modeled the structure as something like:
type Tag{
id: String!
children: [Tag]
}
The problem appears when one wants to get the tags to an arbitrary depth.
To get all the children to (for example) level 3 one would write a query like:
{
tags {
id
children {
id
children {
id
}
}
}
}
Is there a way to write a query to return all the tags to an arbitrary depth?
If not what is the recommended way to model a structure like the one above in a GraphQL API.
Some time ago I came up with another solution, which is the same approach like @WuDo suggested.
The idea is to flatten the tree on data level using IDs to reference them (each child with it's parent) and marking the roots of the tree, then on client side build up the tree again recursively.
This way you should not worry about limiting the depth of your query like in @samcorcos's answer.
schema:
type Query {
tags: [Tag]
}
type Tag {
id: ID!
children: [ID]
root: Boolean
}
response:
{
"tags": [
{"id": "1", "children": ["2"], "root": true},
{"id": "2", "children": [], "root": false}
]
}
client tree buildup:
import find from 'lodash/find';
import isArray from 'lodash/isArray';
const rootTags = [...tags.map(obj => {...obj)}.filter(tag => tag.root === true)];
const mapChildren = childId => {
const tag = find(tags, tag => tag.id === childId) || null;
if (isArray(tag.children) && tag.children.length > 0) {
tag.children = tag.children.map(mapChildren).filter(tag => tag !== null);
}
}
const tagTree = rootTags.map(tag => {
tag.children = tag.children.map(mapChildren).filter(tag => tag !== null);
return tag;
});
Your best bet is to pass a parameter and use that parameter in your resolver. Your syntax will vary depending on which pattern you adopted, but this is the gist of it.
/*
Add an argument to your query:
query {
tags(depth: 3) {
id
children
}
}
*/
export default {
Query: {
tags: async (obj, { depth, }, context) => {
// depending on how you're getting tags, run the function
// that gets you a list of tags
const tags = await getTags(obj, { depth, }, context)
// depending on which ORM you're using, join
// `depth` number of times here on `tags.children`
return tags
}
}
}
Obviously, any time you recursively query like this, you're risking a database explosion, but as long as you know what you're doing, it should be fine.