Hi @chilltemp and welcome to the forums!
I agree that Union types would be very helpful for your use case. It’s on our roadmap to provide them, but we are not there yet, so obviously that won’t help you right now.
A lot will depend on how complex you data is and how much work you want to do to create and maintain a flexible schema. I have a couple of ideas to start, then a much more detailed suggestion. The detailed suggestion goes a bit off the rails, but I hope that it demonstrates some more advanced techniques available to you.
First let’s identify some goals/capabilities of our schema:
- Query for all library items of a given type (Series, Book, Movie, etc.)
- Query for all relationships to/from an item
- Query for all items in a library
- Relationships (edges in the graph) have properties.
Different Link types
If the number of types is obvious, small and not likely to change, this might not be a bad idea
- You can get all items of a type, because each item has its own Collection
- You can query for all relationships by enumerating each link type
- You can query for all items in a library by enumerating each type
- Different link types can have different properties
#4 might be unique to this option, so that is something worth consideration!
Sparse fields
If the data in the different kinds of items are not too different you can put all of the fields in one LibraryItem
type. Only the fields that are required for a given “type” of item are provided. You can provide a string “type” or “tag” field that identifies the item as a certain type of item.
Downsides to this are fairly clear: you lose a great deal of control over type validation in GraphQL. Your app has to do a lot more heavy lifting to validate and manage your various types.
But we satisfy our objectives:
- You can get all items of a type by indexing on the “type” or “tag” field.
- You can query for all relationships through the unified Link type.
- You can query for all items in a library through a single relation field.
- You have one Link type with properties
Example using a “component” pattern
This is similar to the “sparse fields” idea, in that it puts all entities in the same Collection. But we’ll wrap each distinct bit of data into an @embedded
type. This will give us more control over our types and GraphQL queries.
I’ve been thinking about how to model complex relationships in GraphQL for a long time, specifically how to do so without support for Union type. Lacking Union types, and (importantly) for the data I have modeled, I’ve observed that it was easier to describe entities has having a multitude of components, rather than just a single type.
Clearly, not all enties have the same properties: a paperback book does not have a listening length like an audio book. But for a library perhaps all types of a media can share some properties, like if they are “new releases” or are being “promoted” somehow.
Handling nonsensical types could be helped by… Unions! What I would LOVE to do is still have different types represented as a Union (just like you shared). But since we don’t have them, there is some amount of effort that will have to go into avoiding them through the application.
example in graphQL playground
Here is an example of creating some media and querying for it with this pattern:
Create a series with some linked books. These will be “promoted”.
Create another book that is a new release
We can search for all items in the library:
We can search for all items of a given type:
Or by multiple types:
GraphQL Schema
type Library {
key: String! @unique
items: [LibraryItem!] @relation
}
type LibraryItem {
library: Library! @relation
key: String!
inboundLinks: [ItemLink!] @relation(name: "link_target")
outboundLinks: [ItemLink!] @relation(name: "link_source")
components: Components
}
type ItemLink {
# unique == source & target & label
source: LibraryItem! @relation(name: "link_source")
target: LibraryItem! @relation(name: "link_target")
label: String! # ex. "is_in_series", "is_authored_by", "is_reserved_by"
# other link attributes
}
type Components @embedded {
# components with data
book: Book,
series: Series,
movie: Movie,
game: Game,
borrowable: Borrowable
# singleton components
promoted: Boolean
newRelease: Boolean
}
type Book @embedded {
title: String!
pages: Int
}
type Series @embedded {
title: String!
}
type Movie @embedded {
title: String!
}
type Game @embedded {
title: String!
}
type Borrowable @embedded {
total: Int!
available: Int!
}
type Query {
itemsWithComponent(component: String!): [LibraryItem]! @resolver(paginated: true)
itemsWithAllComponents(components: [String!]!): [LibraryItem]! @resolver(paginated: true)
itemsWithAnyComponents(components: [String!]!): [LibraryItem]! @resolver(paginated: true)
}
Fauna Indexes and Functions
An index that can find all items with a given component:
CreateIndex({
name: "LibraryItem_has_component",
source: {
collection: Collection("LibraryItem"),
fields: {
components: Query(Lambda("doc",
Let(
{
components: Select(["data", "components"], Var("doc"), {}),
component_keys: Map(ToArray(Var("components")), c => Select(0, c))
},
Var("component_keys")
)
))
}
},
terms: [
{ binding: "components" }
]
})
When there are multiple @resolver
fields that are paginated, I prefer to reuse a helper UDF:
CreateFunction({
name: "paginate_helper",
body: Query(Lambda(["match", "size", "after", "before"],
If(
Equals(Var("before"), null),
If(
Equals(Var("after"), null),
Paginate(Var("match"), { size: Var("size") }),
Paginate(Var("match"), { size: Var("size"), after: Var("after") })
),
Paginate(Var("match"), { size: Var("size"), before: Var("before") }),
)
))
})
Customer resolvers
CreateFunction({
name: "itemsWithComponent",
body: Query(
Lambda(
["component", "size", "after", "before"],
Let(
{
match: Match(Index("LibraryItem_has_component"), Var("component")),
page: Call("paginate_helper", [
Var("match"),
Var("size"),
Var("after"),
Var("before")
])
},
Map(Var("page"), ref => Get(ref))
)
)
)
})
Use Intersection
to require many components
CreateFunction({
name: "itemsWithAllComponents",
body: Query(
Lambda(
["components", "size", "after", "before"],
Let(
{
match: Intersection(
Map(
Var("components"),
Lambda(
"component",
Match(Index("LibraryItem_has_component"), Var("component"))
)
)
),
page: Call("paginate_helper", [
Var("match"),
Var("size"),
Var("after"),
Var("before")
])
},
Map(Var("page"), ref => Get(ref))
)
)
)
})
Use Union
to select for any of multiple components
CreateFunction({
name: "itemsWithAnyComponents",
body: Query(
Lambda(
["components", "size", "after", "before"],
Let(
{
match: Union(
Map(
Var("components"),
Lambda(
"component",
Match(Index("LibraryItem_has_component"), Var("component"))
)
)
),
page: Call("paginate_helper", [
Var("match"),
Var("size"),
Var("after"),
Var("before")
])
},
Map(Var("page"), ref => Get(ref))
)
)
)
})