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 Todo. You can then use GraphQL queries to access documents in the Todo collection.

In addition to the fields specified in your schema (title, reminders, and completed, in this case), Fauna adds two fields to every object type: _id and _ts. The _id field contains the document ID portion of the Reference which is included with every Fauna document, and _ts is the document’s timestamp.

2 This line defines an embedded object Type named Reminder.

Because it is annotated with the @resolver directive, the GraphQL API does not create a collection for this Type: it exists within the Todo Type. The directive allows you to nest Types within your collection.

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 Todo collection.

Fauna also adds several elements to your schema to assist with pagination of query results. To learn more, see the GraphQL Pagination tutorial.

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 @generateUDFResolvers:

  • @generateUDFResolvers only creates UDFs when they do not already exist.

    This means that if you add the @collection directive to a Type annotated with @generateUDFResolvers, to specify a different collection name, and then re-import your schema, you need to manually update the UDFs to use the new collection name.

  • If you remove a generated UDF, your GraphQL queries fail.

  • If you remove the metadata from a generated UDF, the UDF becomes unavailable to the GraphQL API and your GraphQL queries fail.

    The GraphQL API stores its metadata in the data field of the documents describing the collections, indexes, and UDFs that support your GraphQL schema.

  • If you modify the metadata for a collection, index, or UDF, the modification might become incompatible with other declarations in your schema, which could cause your GraphQL queries to fail.

  • For generated UDFs that return document data, be aware that modifications that restrict which fields are returned could cause GraphQL queries that request the "missing" fields to fail.

  • If you use the @resolver directive to specify a field-level resolver UDF, and you specify a name that matches a UDF generated by @generateUDFResolvers, the UDF generated by @generateUDFResolvers is used instead of the stub function.

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:

  1. Use ABAC roles to prevent the document actions create, read, update, and delete, and enable call so that Mutations all always handled by a UDF.

  2. Prevent document updates by using the @resolver directive.

    Using the @resolver directive requires that a resolver be executed for each Mutation of the associated Type. Since the default resolver is a stub function that aborts queries (you have to provide your own implementation), Mutations are easily blocked.

    To block partial updates, you need to add the @resolver directive to the partial update Mutation. For example:

    type Mutation {
      partialUpdateUser(id: ID!, data: PartialUpdateUserInput!): User @resolver
    }

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:

Supported scalar types

The GraphQL API supports the following built-in types:

  • Boolean: A value that represents true or false.

  • Date: A Date value. The GraphQL API communicates and renders these as strings in the format yyyy-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 the String type, an ID 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 the ID 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 format yyyy-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!