GraphQL schemas
How the GraphQL API adjusts your schema during import
When you import a GraphQL schema into the Fauna Dashboard, the schema defines the object types, their fields and their data types, plus Queries and Mutations that should be available to clients.
Your schema may include object types which map to Fauna
collections and queries which
map to user-defined
functions via the @resolver
directive.
Consider the following example GraphQL schema:
type Todo { (1)
title: String!
reminders: [Reminder]!
completed: Boolean
}
type Reminder @embedded { (2)
timestamp: String!
}
type Query { (3)
allTodos: [Todo!]
todosByCompletedFlag(completed: Boolean!): [Todo!]
}
1 | This line defines an object Type named Todo .
When you import this schema, Fauna creates a collection named In addition to the fields specified in your schema ( |
2 | This line defines an embedded object Type named Reminder .
Because it is annotated with the
|
3 | This line defines two queries for retrieving documents from the
Todo collection, allTodos and todosByCompletedFlag .
The GraphQL API automatically creates the
indexes necessary to execute
those queries. In addition, Fauna also creates several utility
queries to help you perform CRUD operations on the |
Fauna also adds several elements to your schema to assist with pagination of query results.
Click on the SCHEMA tab on the right side of the GraphQL Playground to view the complete list of queries and object types available to you. The left-side navigation of the Fauna Dashboard lets you view all the collections, indexes, and user-defined functions which exist within your database. |
The updated schema looks this:
type Mutation {
createTodo(
data: TodoInput!
): Todo!
updateTodo(
id: ID!
data: TodoInput!
): Todo
deleteTodo(
id: ID!
): Todo
partialUpdateTodo(
id: ID!
data: PartialUpdateTodoInput!
): Todo
}
input PartialUpdateReminderInput {
timestamp: String
}
input PartialUpdateTodoInput {
title: String
reminders: [PartialUpdateReminderInput]
completed: Boolean
}
input ReminderInput {
timestamp: String!
}
input TodoInput {
title: String!
reminders: [ReminderInput]!
completed: Boolean
}
type Query {
findTodoByID(
id: ID!
): Todo
allTodos(
_size: Int
_cursor: String
): TodoPage!
todosByCompletedFlag(
_size: Int
_cursor: String
completed: Boolean!
): TodoPage!
}
type Reminder {
timestamp: String!
}
type Todo {
_id: ID!
_ts: Long!
reminders: [Reminder]!
completed: Boolean
title: String!
}
type TodoPage {
data: [Todo]!
after: String
before: String
}
When a GraphQL schema is imported, the GraphQL API enforces that schema for all subsequent GraphQL queries and mutations, until the schema is modified or replaced. Schema enforcement happens in the GraphQL API only: the schema is not enforced at the database level. FQL queries or UDFs can create or mutate documents, in the collections created for the schema, that do not comply with the schema. Such documents could cause queries and mutations to fail. Care must be taken to ensure schema compatibility when using FQL or UDFs to create/mutate documents in GraphQL collections. |
How the GraphQL API models a schema in a Fauna database
When you import a GraphQL schema, the GraphQL API may perform any or all of the following changes in your database:
Collections
Each Type declared in your schema causes a collection to be created with the Type’s name, except for embedded types. All documents of the same type are stored in a single collection.
Indexes
Each named Query included in your schema causes an index to be created
with the same name. Any field parameters specified for a named Query
causes the index’s terms
definition to include those fields as search
parameters.
Additionally, each @relation
directive
annotation causes an index to be created that represents the
relationship between documents.
User-defined functions for @resolver
directives
Each use of the @resolver
directive causes a stub
UDF to be created for the annotated Query or Mutation. The GraphQL API
cannot automatically infer what the UDF should do, so the stub function
simply calls Abort
with an appropriate error message. You can
then update that function to implement the operations that you require.
For example, if you import the following schema:
type Query {
sayHello: String! @resolver
}
The resulting UDF is created:
Get(Function("sayHello"))
{
ref: Ref(Ref("functions"), "sayHello"),
ts: 1647552077240000,
name: "sayHello",
data: {
// not shown
},
body: Query(
Lambda(
"_",
Abort(
"Function sayHello was not implemented yet. Please access your database and provide an implementation for the sayHello function."
)
)
)
}
UDFs for an @generateUDFResolvers
directive
When you execute a Query or Mutation, the GraphQL API dynamically translates the current query into an FQL query, executes the FQL query, and then marshalls the FQL response into a GraphQL response.
When you use the @generateUDFResolvers
directive,
the GraphQL API persists the dynamically-executed FQL as UDFs. You can
examine the UDFs to help you learn FQL, or you can modify and extend the
FQL to better suit your specific use case.
When the directive is processed during schema import, the GraphQL API
creates the following CRUD UDFs for the annotated Type, except when that
Type is involved in a relationship:
create<Type>
, find<Type>ByID
, update<Type>
, delete<Type>
, and
list<Type>
.
For example, if you import the following schema:
type Cat @generateUDFResolvers {
name: String!
}
The addition of the
@generateUDFResolvers
directive defines
Queries and Mutations that result in the following GraphQL schema:
type Cat {
name: String!
}
type QueryListCatPage {
data: [Cat]!
after: String
before: String
}
type Query {
findCatByID(id: ID!): Cat @resolver(name: findCatByID)
listCat(
_size: Int
_cursor: String
): QueryListCartPage @resolver(name: listCat, paginated: true)
}
type Mutation {
createCat(data: CatInput!): Cat! @resolver(name: createCat)
deleteCat(id: ID!): Cat @resolver(name: deleteCat)
updateCat(
id: ID!
data: CatInput!
): Cat! @resolver(name: updateCat)
}
The resulting UDFs look like:
Map(Paginate(Functions()), Lambda("f", Get(Var("f"))))
{
data: [
{
ref: Ref(Ref("functions"), "deleteCat"),
ts: 1647378852310000,
name: "deleteCat",
data: {
// not shown
},
body: Query(
Lambda(
["id"],
If(
Exists(Ref(Collection("Cat"), Var("id"))),
Delete(Ref(Collection("Cat"), Var("id"))),
null
)
)
)
},
{
ref: Ref(Ref("functions"), "createCat"),
ts: 1647378852310000,
name: "createCat",
data: {
// not shown
},
body: Query(
Lambda(["data"], Create(Collection("Cat"), { data: Var("data") }))
)
},
{
ref: Ref(Ref("functions"), "updateCat"),
ts: 1647378852310000,
name: "updateCat",
data: {
// not shown
},
body: Query(
Lambda(
["id", "data"],
If(
Exists(Ref(Collection("Cat"), Var("id"))),
Update(Ref(Collection("Cat"), Var("id")), { data: Var("data") }),
null
)
)
)
},
{
ref: Ref(Ref("functions"), "findCatByID"),
ts: 1647378852310000,
name: "findCatByID",
data: {
// not shown
},
body: Query(
Lambda(
["id"],
If(
Exists(Ref(Collection("Cat"), Var("id"))),
Get(Ref(Collection("Cat"), Var("id"))),
null
)
)
)
},
{
ref: Ref(Ref("functions"), "listCats"),
ts: 1647378852310000,
name: "listCats",
data: {
// not shown
},
body: Query(
Lambda(
["size", "after", "before"],
Let(
{
setToPageOver: Documents(Collection("Cat")),
page: Paginate(Var("setToPageOver"), {
cursor: If(
Equals(Var("before"), null),
If(Equals(Var("after"), null), null, { after: Var("after") }),
{ before: Var("before") }
),
size: Var("size")
})
},
Map(Var("page"), Lambda("ref", Get(Var("ref"))))
)
)
)
}
]
}
Keep in mind the following concerns when using
|
Partial updates
The GraphQL API automatically generates an Input Type and a Mutation to support partial document updates. This means that when you update a document, you do not have to provide every field that currently exists in order to change one field.
For example, if you import the following schema:
type User {
username: String!
password: String!
}
Then the GraphQL API creates an partialUpdate<Type>
Mutation and a
PartialUpdate<Type>Input
Input Type:
type Mutation {
partialUpdateUser(id: ID!, data: PartialUpdateUserInput!): User
}
type PartialUpdateUserInput {
username: String
password: String
}
All of the fields are optional in the new Input Type, and any required fields are validated at runtime when executing the Mutation.
There is no partial update capability for arrays. When you attempt to mutate an array field, you must provide the entire array definition.
If a partial update Mutation is used to update an array, and the array includes embedded objects, those objects must be fully specified. Currently, when embedded objects have required fields, those requirements are not enforced during a partial update Mutation. This means that it is possible to violate the field requirements during the execution of the Mutation, which would prevent subsequent reading of the document. If you encounter this situation, you would need to correct the problem using FQL queries to update the documents to satisfy the required field constraint(s). |
If you have used the GraphQL API prior to the general availability of the partial updates feature (March 1, 2022), new Mutation types are created when you import a schema (as noted above). If you do not want clients to be able to use the Mutation types, you can:
|
How the GraphQL API process queries
When you send a GraphQL query to the GraphQL API, an FQL query is dynamically created that:
-
enforces the field types of parameters to Mutations, per the field definitions in the schema.
-
resolves field values using either:
-
dynamic FQL that retrieves/updates a field
-
a UDF created by annotating a schema with the
@resolver
directive or@generateUDFResolvers
directive.
-
Supported scalar types
The GraphQL API supports the following built-in types:
-
Boolean
: A value that representstrue
orfalse
. -
Date
: A Date value. The GraphQL API communicates and renders these as strings in the formatyyyy-MM-dd
, but they are stored as FQL dates. -
Float
: A 64-bit floating point number. -
ID
: A string representing a generic identifier. Compared to theString
type, anID
is not intended to be human-readable.If the field specification in your schema includes the
@unique
, the identifier must be unique within the current type.Fauna provides a unique identifier for a document via the
_id
field, which represents the document’s Reference. You would typically use theID
type for documents that have an externally-created identifier, such as documents imported from another database). -
Int
: A 32-bit signed decimal integer number. -
Long
: A 64-bit signed decimal integer number. -
String
: A string of UTF-8 characters. -
Time
: A Timestamp value. The GraphQL API communicates and renders these as strings in the formatyyyy-MM-ddTHH:mm:ss.SSSZ
, but they are stored as FQL timestamps.Fauna provides a document’s most recent modification timestamp via the
_ts
field, which has microsecond resolution.
Is this article helpful?
Tell Fauna how the article can be improved:
Visit Fauna's forums
or email docs@fauna.com
Thank you for your feedback!