Migrating your GraphQL schema with Fauna
Tags: fauna, database, migrations, graphql
Published: 2021-09-10 19:00:26

Modern applications frequently change as you deliver features and fixes to customers. In this series, you learn how to implement migrations in [Fauna](https://fauna.com), the data API for modern applications. 

The [first post in this series](https://dev.to/fauna/evolving-the-structure-of-your-fauna-database-5704) introduces a high-level strategy for planning and running migrations. The [second post](https://dev.to/fauna/migrating-with-user-defined-functions-12cp) demonstrates how to implement migration patterns using [user-defined functions (UDFs)](https://docs.fauna.com/fauna/current/api/fql/user_defined_functions) written in the [Fauna Query Language (FQL)](https://docs.fauna.com/fauna/current/api/fql/). In this post, you learn specific considerations for migrating Fauna databases that you access via GraphQL and how to apply the techniques from the previous posts.

All of the code in this series is available on GitHub in [Fauna Labs](https://github.com/fauna-labs/migrations).

## Migration scenario

This post implements the same migration scenario you used in the previous post. Your Fauna database has a collection for firewall rules and you have an updated requirement to manage inbound traffic from *an arbitrary number* of IP address ranges. To satisfy this requirement, you must migrate the *ipRange* field type from an [FQL string](https://docs.fauna.com/fauna/current/api/fql/types#string) to an [FQL array](https://docs.fauna.com/fauna/current/api/fql/types#array) of strings.

### Pre-requisites

To follow along with this post you must have access to a Fauna account. You can [register for a free Fauna account](https://dashboard.fauna.com/accounts/register) and benefit from [Fauna’s free tier](https://fauna.com/pricing) while you learn and build. You do not need to provide payment information until you upgrade your plan.

You do not need to install any additional software or tools. All examples in this post can be run in the web shell and the GraphQL playground in the [Fauna dashboard](https://dashboard.fauna.com).

This post assumes you have read and understood the previous posts in this series.

### Create and populate your database

Create another new database in the [Fauna dashboard](https://dashboard.fauna.com). Do not select the "Pre-populate with demo data" checkbox, and do not re-use the database from the previous post.

Save [`initial-schema.graphql`](https://github.com/fauna-labs/migrations/blob/main/graphql/schemas/initial-schema.graphql) to your computer. In your new database in the Fauna dashboard, select the *GraphQL* tab, choose *Import Schema*, and upload *initial-schema.graphql*.

```graphql
type FirewallRule {
  action: String!
  port: Int!
  ipRange: String!
  description: String
}
```

Navigate to the *Collections* tab and notice that Fauna creates an empty *FirewallRule* collection.

Navigate to the *Functions* tab and choose *New Function*. Enter *migrate_firewall_rule* as the *Function Name* and leave the *Role* set to the default *None*. Paste the following FQL in the field *Function Body* and choose *Save* to create your UDF. Note that this is the same UDF you created in the previous post.

**`migrate_firewall_rule`**

```
Query(
  Lambda(
    ["firewall_rule_ref"],
    Let(
      { 
        doc: Get(Var("firewall_rule_ref")),
        ipRange: Select(["data", "ipRange"], Var("doc"))
      },
      If(
        IsArray(Var("ipRange")),
        Var("doc"),
        Update(
          Var("firewall_rule_ref"),
          { data: { ipRange: [Var("ipRange")] } }
        )
      )
    )
  )
)
```

Return to the *GraphQL* tab, copy the mutation from [`populate-firewall-rules.graphql`](https://github.com/fauna-labs/migrations/blob/main/graphql/populate-firewall-rules.graphql), and paste it into the query editor. Choose the "play" button and Fauna runs your mutation and returns the `_id` and `description` for each sample firewall rule.

Return to the *Collections* tab and confirm that your *FirewallRule* collection now contains three documents.

## Encapsulation

Encapsulation is built into GraphQL via strongly typed queries and mutations. Choose *Schema* in the GraphQL Playground and review the modified schema that Fauna generates for your database. Note that Fauna makes the following relevant modifications to your schema:

* Copies the `type FirewallRule` you provide to an `input FirewallRule`.
* Adds `_id` and `_ts` fields to the `type FirewallRule` you provide.
* Generates one GraphQL query - `findFirewallRuleById`.
* Generates three GraphQL mutations - `createFirewallRule`, `updateFirewallRule`, and `deleteFirewallRule`.

You can override or redefine everything that Fauna generates for you, including GraphQL `input` types. You can also [specify UDFs as custom resolvers for both queries and mutations](https://docs.fauna.com/fauna/current/api/graphql/functions). Together, these characteristics form the basis of your migration strategy with GraphQL.

## Migrating in steps

You must create UDFs for each relevant query and mutation the first time you perform a migration with GraphQL. Except for the final step, each individual step leaves your database in an equivalent state that does not require any downtime.

For your first migration, you perform the following steps in order:

1. Specify a resolver for your query.
1. Specify your `input`.
1. Specify a resolver for each mutation.
1. Migrate in one step:
  * Modify your UDFs to accept the new shape of your data.
  * Modify your `type` and `input` definitions in your GraphQL schema and replace your schema in Fauna.

You perform subsequent migrations by modifying the relevant UDFs and uploading a new schema in a single step. 

> **Tip**: Use tooling for the final step! Fauna provides the [Fauna Schema Migrate](https://github.com/fauna-labs/fauna-schema-migrate) tool and a [Serverless Framework plugin](https://github.com/fauna-labs/serverless-fauna) to help you manage your resources in Fauna as code.

For all migrations, it is less risky if you perform your migration during an application downtime period. If you are using infrastructure as code (IaC) tools, a single migration should typically take only a few seconds.

### Step one - Specify your first resolver

The first change you make to your database is to explicitly specify a UDF as a resolver for your query. You modify your query before your mutations because it does not require specifying an `input`. This way you change only one thing at a time and check for correctness, supporting the principle of *migrating in steps*.

Return to the *Functions* tab and again choose *New Function*. This time enter *find_firewall_rule_by_id* as the *Function Name* and leave the *Role* set to the default *None*. Paste the following FQL in the field *Function Body* and choose *Save* to create your UDF.

**`find_firewall_rule_by_id`**
```
Query(
  Lambda(
    ["id"],
    Get(
      Ref(Collection("FirewallRule"), Var("id"))
    )
  )
)
```

Open your schema and explicitly define the `findFirewallRuleByID` query by adding the following definition:

```graphql
type Query {
  findFirewallRuleByID(id: ID!): FirewallRule @resolver(name: "find_firewall_rule_by_id")
}
```

There are two points to note about this definition:

1. Everything except for `@resolver(name: "find_firewall_rule_by_id")` is copied directly from the schema that Fauna generates. This means you are not *creating* a new query, but are *directing* an existing query to use a specific UDF with the `@resolver` directive.
1. The `@resolver` directive takes one parameter, `name`, whose value is the name of the UDF Fauna invokes to process the query.

Save your schema and return to the *GraphQL* tab. Choose *Replace Schema*, choose *Replace*, and select your modified schema.

### Step two - Create an *input*

When you upload a GraphQL schema, Fauna copies any `type` definitions to a corresponding `input` definition. If you explicitly define this input in your schema, Fauna uses the definition you specify.

Copy the following `input FirewallRuleInput` definition and add it to your schema:

```graphql
input FirewallRuleInput {
  action: String!
  port: Int!
  ipRange: String!
  description: String
}
```

Note this definition is identical to the generated schema in your Fauna dashboard. For your own migrations, you can copy the `input` definition from the schema Fauna displays in the *GraphQL* tab.

Save your schema again, return to the *GraphQL* tab, and replace your schema with the newest version.

### Step three - Specify resolvers for mutations

Now that you have an `input` definition, you can specify resolvers for your mutations. Create UDFs for each mutation using the following FQL:

**`create_firewall_rule`**
```
Query(
  Lambda(
    ["data"],
    Create(
      Collection("FirewallRule"),
      Var("data")
    )
  )
)
```

**`update_firewall_rule`**
```
Query(
  Lambda(
    ["id", "firewall_rule_input"],
    Update(
      Ref(Collection("FirewallRule"), Var("id")),
      { data: Var("firewall_rule_input") }
    )
  )
)
```

**`delete_firewall_rule`**
```
Query(
  Lambda(
    "id",
    Delete(
      Ref(Collection("FirewallRule"), Var("id"))
    )
  )
)
```

Add the following to your schema to specify resolvers for each mutation:

```graphql
type Mutation {
  createFirewallRule(data: FirewallRuleInput): FirewallRule @resolver(name: "create_firewall_rule")
  updateFirewallRule(id: ID!, data: FirewallRuleInput!): FirewallRule @resolver(name: "update_firewall_rule")
  deleteFirewallRule(id: ID!): FirewallRule @resolver(name: "delete_firewall_rule")
}
```

Save your schema and replace the schema in the *GraphQL* tab of the Fauna dashboard. At this point, you have *exactly* the same functionality you started with. Because each query and mutation has an explicitly defined UDF as its resolver, you are now ready to perform your migration.

### Final schema - migrate all at once

The final step in a migration is to update the logic in your UDFs, modify your `type` and `input` definitions in your schema, and upload your schema to Fauna. Because GraphQL is strongly typed, you must complete these actions in a single step. The simplest way to do this is by accepting some small amount of downtime for your application while you migrate, typically less than one minute. Techniques for migrating with zero downtime are beyond the scope of this post.

First, update the logic in your UDFs to handle the new shape of your data, reusing the same UDFs from the previous post.

> **Note**: You do not have to update the `create_firewall_rule` UDF! Because GraphQL enforces the schema, any call to the `createFirewallRule` mutation already has the proper updated shape.

**`find_firewall_rule_by_id`**
```
Query(
  Lambda(
    ["id"],
    Call(
      "migrate_firewall_rule", 
      Ref(Collection("firewall_rules"), Var("id"))
    )
  )
)
```

**`update_firewall_rule_by_id`**
```
Query(
  Lambda(
    ["id", "new_rule"],
    Let(
      {
        ref: Ref(Collection("migrate_firewall_rule"), Var("id")),
        doc: Update(
          Var("ref"),
          Var("new_rule")
        )
      },
      Call("migrate_firewall_rule", Var("ref"))
    )        
  )
)
```

**`delete_firewall_rule`**
```
Query(
  Lambda(
    ["id"],
    Let(
      {
        ref: Ref(Collection("migrate_firewall_rule"), Var("id")),
        doc: Call("migrate_firewall_rule", Var("ref"))
      },
      Delete(Var("ref"))
    )
  )
)
```

Next, modify the `type` and `input` declarations in your GraphQL schema to reflect the new shape of your data:

```graphql
type FirewallRule {
  action: String!
  port: Int!
  ipRange: [String]!
  description: String
}

input FirewallRuleInput {
  action: String!
  port: Int!
  ipRange: [String]!
  description: String
}
```

Finally, save your schema and replace the schema in the *GraphQL* tab of the Fauna dashboard. That's it!

### Validating the migration

You can follow the steps laid out in the section [Confirming zero defects](https://dev.to/fauna/migrating-with-user-defined-functions-12cp#confirming-zero-defects) in the previous post to verify that your migration was successful.

You can also use GraphQL queries and mutations to confirm that you observe the expected behavior. Navigate to the *Collections* tab in the Fauna dashboard and copy the `id` of an existing, unmigrated document in the *FirewallRule* collection. Return to the *GraphQL* tab and run the following query, replacing `<FIREWALL_RULE_ID>` with the `id` you copied:

```graphql
query {
  findFirewallRuleByID(id:"<FIREWALL_RULE_ID>"){
    description
    ipRange
    port
    action
  }
}
```

The firewall rule should be displayed with an array value for *ipRange*. Return to the *Collections* tab and verify that the document now has an array value for *ipRange*.

## Conclusion

When performing migrations with GraphQL, you apply the same principles that you apply when migrating with FQL. Before you perform your first GraphQL migration, you must specify `input`, `type`, and `@resolver` definitions in your GraphQL schema. Because GraphQL strongly enforces types via the schema definition, GraphQL migrations typically require some small amount of downtime as you replace your UDFs and schema.

This post leaves your underlying data unchanged until you access it, a pattern known as "just-in-time" (JIT) data migration. The final post in this series discusses JIT and two additional techniques for migrating your data and indexes with Fauna, along with sample code and guidance on choosing an appropriate approach for your application.