Alexander's Swift Blog

Using Enums with GraphQL and Swift

Enums are a powerful language feature of Swift. They are more than a type-safe way of defining different states. But in this article, we'll concentrate on the basic enum functionality in Swift and we'll integrate them in a GraphQL API server which is based on Vapor. If you are new to GraphQL in combination with Vapor, I recommend reading my previous article where I go through the steps on how to build a GraphQL server with Swift and Vapor.

In this article, we'll extend the todo API example. Therefore we use a GraphQL Vapor template as a starting point. We want to build the feature of marking a todo as done or moving it to a "for later" list. To represent the different states of a todo we make use of enums. We'll save this state property with the rest of the information in a database and make them available through an extension to the GraphQL API that is already in place.

Creating the project

So let's get started by creating a new Vapor project based on the GraphQL templated we build up in the previous article. Therefore we use the Vapor toolchain but instead of creating a project from the default template we specify the repository of the GraphQL template: vapor new todo-server --template=alexsteinerde/vapor-graphql-template

TodoState enum

Now we can add our enum which represents the different states our todo can be in: TodoState. As it belongs to the Todo class we can add it as a sub-enum. We let the enum conform to String so it's prepared to be Codable and also to CaseIterable which allows us to automatically infer all possible cases when registering it in our GraphQL schema later.

extension Todo {
		enum TodoState: String, CaseIterable {
		    case open
		    case done
		    case forLater
		}
}

Extended Todo Model

We extend our existing Todo model to store the state as a property and also add support for saving the state to a Fluent database.

/// A single entry of a Todo list.
final class Todo: SQLiteModel {
		... other properties
 
    var state: TodoState

    /// Creates a new `Todo`.
    init(id: Int? = nil, title: String, state: TodoState = .open) {
        self.id = id
        self.title = title
        self.state = state
    }
}

We add the property state and add a new constructor parameter that can be pass in but is set to .open by default.

To support database storage of the TodoState in the used SQLite database we need to extend it and make it conform to Content and SQLiteEnumType. The first protocol is familiar in the Vapor world. But SQLiteEnumType is something that isn't used every day. This protocol requires us to implement the reflectDecoded() method. We need to provide two unique cases of our enum to make storage possible. Here we return the cases .done and .open. But we could also use .open and .forLater.

/// Allows `Todo.TodoState` to be stored in the database.
extension Todo.TodoState: Content, SQLiteEnumType {
    static func reflectDecoded() throws -> (Todo.TodoState, Todo.TodoState) {
        return (.done, .open) // Provide two unique cases
    }
}

By default, we use the SQLite Fluent database which stores data in-memory. If you want to use MySQL, PostgreSQL or other databases you can look up the requirements of a storable enum at the Vapor documentation or in the Vapor Discord channel.

Business Logic API

Next, we write the business logic that lets us change the state of a todo. To identify the todo we want to change we use the id. The id and the new state are our two arguments. They can be found in the struct ChangeTodoStateArguments.

extension TodoAPI {
		struct ChangeTodoStateArguments: Codable {
		    let id: Int
		    let state: Todo.TodoState
		}
		
		func changeTodoState(request: Request, arguments: ChangeTodoStateArguments) throws -> EventLoopFuture<Todo> {
		    Todo.find(arguments.id, on: request)
		        .unwrap(or: Abort(.notFound))
		        .map ({ (todo) in
		            todo.state = arguments.state
		            return todo
		        })
		        .save(on: request)
		}
}

The function changeTodoState accepts the current request to use for database operations and the arguments. It returns the updated Todo as a future. To update the todo we first search for the given id. If no entry is found we throw a notFound error. Otherwise, we replace the old state with the new one and save it to the database by calling .save(on: request).

Field Keys

To add the new state property to our GraphQL Todo type we first need to define the state case of the Todo.FieldKeys enum:

extension Todo {
		enum FieldKeys: String {
		    case id
		    case title
		    case state // New state property
		}
}

And we also add a new case to the TodoAPI.FieldKeys enum which we call changeTodoState. This will represent the name of the mutation to change the state of a todo.

extension TodoAPI {
		enum FieldKeys: String {
			  // Names for the GraphQL schema endpoints
		    ...
		    case changeTodoState // Mark a todo as done, open or forLater
		}
}

We don't need to define the names for the arguments as field keys if we don't want to add descriptions to each argument.

Schema

Now let's modify our schema instance in the Schema.swift file. We need to add the enum type to our Schema (1). It must be defined before any other usage of the TodoState type! The Enum constructor normally needs an array of Values that represent the cases the enum has. But with GraphQLKit an extension to the type is provided that allows enums that conform to CaseIterable to be passed directly without a list of values to the constructor. Secondly, we add the new Field state to our Todo type (2). And to switch between states we add the new mutation changeTodoState at the end (3). As mentioned earlier, we only need to provide each parameter to the mutation field if we want to add descriptions. Otherwise, it is inferred automatically by the argument type of the changeTodoState function.

// Definition of our GraphQL schema.
let todoSchema = Schema<TodoAPI, Request>([
    // Todo type with it's fields
    Enum(Todo.TodoState.self), // 1) NEW

    Type(Todo.self, fields: [
        Field(.title, at: \.title),
        Field(.id, at: \.id),
        Field(.state, at: \.state) // 2) NEW: Todo state field
    ]),

    // We only have one single query: Getting all existing todos
    Query([
        Field(.todos, with: TodoAPI.getAllTodos),
    ]),

    // Both mutations accept arguments.
    // First we define the name from our FieldKey enum in the TodoAPI
    // and we pass the keypath to the field of the argument struct.
    Mutation([
        Field(.createTodo, with: TodoAPI.createTodo),
        Field(.deleteTodo, with: TodoAPI.deleteTodo),
        Field(.changeTodoState, with: TodoAPI.changeTodoState), // 3) NEW: Change Todo State API
    ]),
])

Example Queries

Now that we have our API ready we can run the server and go to http://localhost:8080 to execute queries against our GraphQL API.

// Getting all todos with it's id, title and state
query GetAllTodos {
  todos {
    id
    title
    state
  }
}

// Creating a new todo sets the state to `open` by default
mutation CreateNewTodo {
  createTodo(title: "Test") {
    id
    title
    state
  }
}

// After creating a todo we can mark it as `done`.
// Or instead of `done` we could also pass `open` or `forLater` here.
mutation MarkTodoeAsDone {
  changeTodoState(id: 1, state: done) {
    id
    state
  }
}

Conclusion

This time we added enum functionality to GraphQL. We build on top of the previous template and the end result can be found in a separate GitHub repository.

What we haven't implemented is creating a new todo with a given state via GraphQL. But this isn't too complicated and doesn't require too many changes. Take it as a challenge and try implementing this by yourself and maybe you come up with some great results I haven't thought about yet.

What are other use cases you have when developing your APIs? Do you think GraphQL can be an advantage there? Let me know your thoughts in the comments, on Twitter or via email.

Tagged with: