Documentation
Basics
Nodes

Nodes

The node function is a core abstraction in fuse, it allows you to express a way to load a key-able* entity and its shape. By defining the way to load the entity we enable a few use-cases

  • We can return the key of the node at any point in our graph and the load function defined on the node will take care of resolving the full entity
  • We can return a list of keys when our list endpoint does not return the full entity and the load function will take care of loading these in parallel.

A few things this endpoint does for you is create an automatic entry-point for the node query-field as well as the lower-cased type query-field (in this case user) and we'll ensure that the id of this entity is globally unique.

Let's look at the example of the Getting Started guide:

import { node } from 'fuse'
 
type UserSource = {
  id: string
  name: string
  avatarUrl: string
}
 
export const UserNode = node<
  UserSource,
  // This is the default, you can change this when you use a different type, like number.
  // string
>({
  name: 'User',
  // This is the default, however if you use a different key-field property you can change this.
  // key: 'id',
  load: async (ids) => getUsers(ids),
  fields: (t) => ({
    name: t.exposeString('name'),
    avatarUrl: t.exposeString('avatarUrl'),
    firstName: t.string({
      resolve: (user) => user.name.split(' ')[0],
    }),
  }),
})

If we were to have a list endpoint that only returned the name and was missing avatarUrl we could do the following:

addQueryFields((t) => ({
  users: t.field({
    type: [UserNode],
    resolve: async () => {
      const result = await listUsers()
      return result.map(user => user.id);
    }
  }),
}))

Now the underlying API knows it needs to go back to the load function and resolve all the details for these keys. Similarly if we were to have an objectField that needs to return a UserNode we can just return the id and the dataloader (opens in a new tab) will ensure that these are loaded in parallel.

*key-able: A key-able entity is an entity that has a unique identifier that can be used to load the entity.

Example of querying the automatically generated entry-points:

query {
  node(id: x) {
    ... on User { id firstName }
  }
  user(id: x) {
    id
    firstName
  }
}

Connecting nodes

Referencing other nodes

For 1:1 relationships you can add a field to your node that returns the key of the related node. In doing so it will take the key and invoke the load function of the related node.

export const BlogPostNode = node<BlogPostSource>({
  name: 'BlogPost',
  load: async (ids) => fetchBlogPosts(ids),
  fields: (t) => ({
    // Exose a field as-is
    title: t.exposeString('title'),
    author: t.field({
      // This refers to the UserNode we created earlier,
      // the id will be used to load the full object.
      type: UserNode,
      resolve: (blogPost) => blogPost.author_id
    })
  }),
})

The benefit here is that when your parent is a list that all of the related nodes will be loaded in parallel.

Extending nodes

When we have a more complex relationship or don't have the key of the related entity at hand we can choose to extend the node.

import { addNodeFields } from 'fuse';
 
addNodeFields(UserNode, t => ({
  blogPosts: t.field({
    type: [BlogPostNode],
    resolve: (parent) => fetchBlogPostsByAuthor(parent.id)
  })
}))

This adds a new field to the node named blogPosts and the resolver will fetch all the blog posts of the user.

If we want to optmise this to load in parallel as well we can use t.loadableList instead of t.field.

How to handle authorization

Authorization can be hard to reason about in these cases, this is why we thought about a few ways to handle this.

You can centralise authorization to the node of an entity, this would mean implementing the logic in the load function. This means that every time you need to go to the load function to resolve an entity it will run through the authorization logic by default. This does mean that if you need a different contextual authority you can't just return the identifier.

Alternatively if you know that the underlying datasource is already running authorization you can choose to defer that logic to the datasource.