Objects, Enums, Unions, and Interfaces
These four are common building blocks for APIs. Let's take a look at how fuse makes them easy to use for you.
Objects
In the nodes we talked about keyed entities and that we can load them
by means of their key. This doesn't cover all the cases, you won't want to expose a node
interface/... for every given entity in your graph and some of them might not have unique
keys.
In comes the objectType, let's look at an example where our User entity has an
Address associated with it.
import { addObjectFields, objectType } from 'fuse'
const Address = objectType<Address>({
name: 'Address',
fields: (t) => ({
street: t.exposeString('name'),
houseNumber: t.exposeInt('houseNumber'),
city: t.exposeString('city'),
country: t.exposeString('country'),
}),
})
addObjectFields(UserNode, t => ({
address: t.field({
type: Address,
resolve: (parent) => {
// fetch address for parent.id (the user id)
}
})
}))The Address has no primary key, but is contextual to the user and
we'll fetch it in the context of our parent user.
You can still integrate dataloader here by switching out the
t.field with t.loadable or t.loadableList if the user would
have multiple addresses.
import { addNodeFields } from 'fuse'
addNodeFields(UserNode, (t) => ({
address: t.loadable({
type: Address,
load: ids => {
/** Load the addresses for all given user-ids */
}
resolve: (parent, args) => {
/** Return the contextual user-id */
return parent.id
}
}),
}))Enums
There are types where you want to narrow down the possible values for a
given field, this can be done by means of the enumType. In this case
let's give our BlogPost a state, this can be published or draft:
import { enumType, addNodeFields } from 'fuse'
const PostStatus = enumType({
name: 'PostStatus',
// The "as" const is important so TypeScript knows
// this array can't change and can easily give you
// type-hints.
values: ['PUBLISHED', 'DRAFT', 'UNKNOWN'] as const,
})
addNodeFields(BlogPostnode, t => ({
status: t.field({
type: PostStatus,
resolve: (parent) => {
if (parent.published) {
return 'PUBLISHED'
} else if (parent.draft) {
return 'DRAFT'
}
return 'UNKNOWN'
}
})
}))Now when the types are generated on the front-end it will know
that post.status can have these three given values.
Interfaces
Our API also supports inheritance, with interfaces we can define
a common shape across objects. Think about the built-in Node interface
which dictates that any node implements the id property.
We can go further and define interfaces ourself as well, let's look
at an example
import { node, interfaceType } from 'fuse'
const ContentNode = interfaceType({
name: 'ContentNode',
fields: (t) => ({
title: t.string()
}),
})
const BlogPostNode = node<{ id: string; title: string; content: string }>({
name: 'BlogPost',
interfaces: [ContentNode],
load: async (ids) => [],
isTypeOf: (parent: any) => {
return !!parent.content
},
fields: (t) => ({
title: t.exposeString('title'),
content: t.exposeString('content'),
}),
})
addQueryFields(t => ({
content: t.field({
type: [ContentNode],
resolve: () => []
})
}))In the above example we define a ContentNode that tells us that every implemeentor
needs to have a title property of type string, then we go on to create our content
entry-point and tell us that any returned value here can be an implementor of the
ContentNode type. On the BlogPost you'll see a isTypeOf function, this is needed
to tell which implementor of the interface is being returned.
We can query this by doing
query {
content {
title
... on BlogPost {
id content
}
__typename
}
}Checking the __typename on the front-end will narrow down the type and you can
add logic based on the specific concrete type.
Unions
Some endpoints will return multiple possible types, with unions you can
catch this case. Let's look at a case where we got an entry-point named
content, this can return blogposts or advertisements
On the
Contenttype you can see aresolveTypefunction, this is an alternative to the aforementionedisTypeOffunction from theinterfaceTypesection.
import { node, unionType } from 'fuse'
const BlogPostNode = node<{ id: string, content: string }>({
name: 'BlogPost',
load: async (ids) => [],
fields: (t) => ({
content: t.exposeString('content'),
}),
})
const AdvertisementNode = node<{ id: string, title: string }>({
name: 'Advertisement',
load: async (ids) => [],
fields: (t) => ({
title: t.exposeString('title'),
}),
})
const Content = unionType({
name: 'Content',
types: [BlogPostNode, AdvertisementNode],
// This is needed so we can tell the field what type
// we are dealing with.
resolveType(blogOrAdvertisement) {
if (blogOrAdvertisement.title) {
return 'Advertisement'
} else {
return 'BlogPost'
}
},
})
addQueryFields(t => ({
content: t.field({
type: [Content],
resolve: () => []
})
}))We can query this by doing
query {
content {
... on BlogPost {
id content
}
... on Advertisement {
id title
}
__typename
}
}Checking the __typename on the front-end will narrow down the type and you can
add logic based on the specific concrete type.