Skip to content

Latest commit

 

History

History
616 lines (523 loc) · 14.7 KB

UsageGuide.md

File metadata and controls

616 lines (523 loc) · 14.7 KB

Usage Guide

The following sections build up a Graphiti schema and detail how to use some of the main features.

Hello World

Here is an example of a basic "Hello world" GraphQL schema:

import Graphiti
import NIO

let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

struct HelloResolver {
    func hello(context: NoContext, arguments: NoArguments) -> String {
        return "world"
    }
}

struct HelloAPI : API {
    typealias ContextType = NoContext
    let resolver = HelloResolver()
    let schema = try! Schema<HelloResolver, NoContext> {
        Query {
            Field("hello", at: HelloResolver.hello)
        }
    }
}

This schema can be queried in Swift using the execute function. :

let result = try await HelloAPI().execute(
    request: "{ hello }",
    context: NoContext(),
    on: eventLoopGroup
)
print(result)

The result of this query is a GraphQLResult that encodes to the following JSON:

{ "hello": "world" }

Swift Types

Graphiti includes support for using Swift types in the schema itself. To connect the Swift type with the GraphQL one, include a Type block in the API declaration, composed of Fields. For example, we can integrate a Person object into the API:

import Graphiti
import NIO

let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

struct Person: Codable {
    let name: String
    let age: Int
    let height: Double
}

let characters = [
    Person(name: "Johnny Utah", age: 23, height: 1.85),
    Person(name: "Bodhi", age: 27, height: 1.8),
]

struct PersonResolver {
    func people(context: NoContext, arguments: NoArguments) -> [Person] {
        return characters
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("name", at: \.name)
            Field("age", at: \.age)
            Field("height", at: \.height)
        }
        Query {
            Field("people", at: PersonResolver.people)
        }
    }
}

let result = try await PointBreakAPI().execute(
    request: """
    {
      people {
        name
        age
      }
    }
    """,
    context: NoContext(),
    on: eventLoopGroup
)

The result above could be decoded to a JSON of the form:

{
  "people" : [
    {
      "name" : "Johnny Utah",
      "age" : 23
    },
    {
      "name" : "Bodhi",
      "age" : 27
    }
  ]
}

Arguments

Arguments can be defined within an API using the Argument initializer in a Field builder. Adjusting our previous example, we can add in an argument to filter people by their age.

struct PeopleArguments: Codable {
    let olderThan: Int
}

struct PersonResolver {
    func people(context: NoContext, arguments: PeopleArguments) -> [Person] {
        return characters.filter { $0.age > arguments.olderThan }
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("name", at: \.name)
            Field("age", at: \.age)
            Field("height", at: \.height)
        }
        Query {
            Field("people", at: PersonResolver.people) {
                Argument("olderThan", at: \.olderThan)
            }
        }
    }
}

A request string for this might be:

{
  people(olderThan: 25) {
    name
  }
}

which would generate the response:

{
  "people" : [
    {
      "name" : "Bodhi"
    }
  ]
}

Mutations

Mutations are defined using a Mutation block in the API, and are typically used to change an underlying dataset. We can expand our example to include a mutation that creates a new person:

struct NewPersonArguments: Codable {
    let name: String
    let age: Int
    let height: Double
}

struct PersonResolver {
    func people(context: NoContext, arguments: NoArguments) -> [Person] {
        return characters
    }
    func newPerson(context: NoContext, arguments: NewPersonArguments) -> Person {
        return Person(
            name: arguments.name,
            age: arguments.age,
            height: arguments.height
        )
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("name", at: \.name)
            Field("age", at: \.age)
            Field("height", at: \.height)
        }
        Query {
            Field("people", at: PersonResolver.people)
        }
        Mutation {
            Field("newPerson", at: PersonResolver.newPerson) {
                Argument("name", at: \.name)
                Argument("age", at: \.age)
                Argument("height", at: \.height)
            }
        }
    }
}

A request string for this might be:

mutation {
  newPerson(name: "Tyler Endicott", age: 22, height: 1.63) {
    name
  }
}

which would generate the response:

{
  "newPerson" : {
    "name" : "Tyler Endicott"
  }
}

Input Objects

Sometimes we'd like to pass a complex argument. Inputs allow us to do this and are declared by including an Input block in the API declaration, composed of InputFields. Our example can be changed to include a mutation that creates multiple new people, each passed as an input object:

struct InputPerson: Codable {
    let name: String
    let age: Int
    let height: Double
}

struct NewPeopleArguments: Codable {
    let individuals: [InputPerson]
}

struct PersonResolver {
    func people(context: NoContext, arguments: NoArguments) -> [Person] {
        return characters
    }
    func newPeople(context: NoContext, arguments: NewPeopleArguments) -> [Person] {
        return arguments.individuals.map { person in
            Person(
                name: person.name,
                age: person.age,
                height: person.height
            )
        }
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("name", at: \.name)
            Field("age", at: \.age)
            Field("height", at: \.height)
        }
        Input(InputPerson.self) {
            InputField("name", at: \.name)
            InputField("age", at: \.age)
            InputField("height", at: \.height)
        }
        Query {
            Field("people", at: PersonResolver.people)
        }
        Mutation {
            Field("newPeople", at: PersonResolver.newPeople) {
                Argument("individuals", at: \.individuals)
            }
        }
    }
}

A request might look like:

mutation {
  newPeople(individuals: [
    {name: "Tyler Endicott", age: 22, height: 1.63},
    {name: "Angelo Pappas", age: 45, height: 1.91},
  ]) {
    name
  }
}

which would generate the response:

{
  "newPeople" : [
    {
      "name" : "Tyler Endicott"
    },
    {
      "name" : "Angelo Pappas"
    }
  ]
}

Subscriptions

Subscriptions are reactive queries that return a result whenever an event occurs. This functionality is built on Swift Concurrency using AsyncThrowingStream. To create a subscription, include a Subscription block in the API declaration composed of SubscriptionFields. We can change our example API to include a subscription alert:

import Foundation
import GraphQL

let timer: Timer!

struct PersonResolver {
    func people(context: NoContext, arguments: NoArguments) -> [Person] {
        return characters
    }
    func fiftyYearStormAlert(context: NoContext, arguments: NoArguments) -> ConcurrentEventStream<String> {
        let asyncStream = AsyncThrowingStream<String, Error> { continuation in
            timer = Timer.scheduledTimer(
                withTimeInterval: 60 * 60 * 24 * 365 * 50,
                repeats: true
            ) { _ in
                continuation.yield("A 50-year storm is occurring!")
            }
        }
        return ConcurrentEventStream<String>.init(asyncStream)
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("name", at: \.name)
            Field("age", at: \.age)
            Field("height", at: \.height)
        }
        Query {
            Field("people", at: PersonResolver.people)
        }
        Subscription {
            SubscriptionField(
                "fiftyYearStormAlert",
                at: FiftyYearStorm.message,
                atSub: PersonResolver.fiftyYearStormAlert
            )
        }
    }
}

This schema can be subscribed to in Swift using the subscribe function. The example below illustrates this and prints the result on each occurance (To see results, you should probably change the timer to execute on a period faster than 50 years):

let api = PointBreakAPI()
let stream = try await api.subscribe(
    request: "subscription { fiftyYearStormAlert }",
    context: NoContext(),
    on: eventLoopGroup
).stream!
let resultStream = stream.map { result in
    try print(result.wait())
}

Each time an event fires, the following message will be generated:

{
  "fiftyYearStormAlert": "A 50-year storm is occurring!"
}

Cursor Connections

This package supports pagination using the Relay-based GraphQL Cursor Connections Specification. To use this pagination style you must:

  1. Ensure any node types implement the Identifiable protocol (they must have a unique id field)
  2. Change the relevant resolver types to use PaginationArguments and return a Connection
  3. Add the PaginationArguments arguments to the schema declaration

Here's an example using the schema above:

struct Person: Codable, Identifiable {
    let id: Int
    let name: String
}

let characters = [
    Person(id: 1, name: "Johnny Utah"),
    Person(id: 2, name: "Bodhi"),
]

struct PersonResolver {
    func people(context: NoContext, arguments: PaginationArguments) throws -> Connection<Person> {
        return try characters.connection(from: arguments)
    }
}

struct PointBreakAPI : API {
    typealias ContextType = NoContext
    let resolver = PersonResolver()
    let schema = try! Schema<PersonResolver, NoContext> {
        Type(Person.self) {
            Field("id", at: \.id)
            Field("name", at: \.name)
        }
        ConnectionType(Person.self)
        Query {
            Field("people", at: PersonResolver.people) {
                Argument("first", at: \.first)
                Argument("last", at: \.last)
                Argument("after", at: \.after)
                Argument("before", at: \.before)
            }
        }
    }
}

A request string for this might be:

{
    people {
        edges {
            cursor
            node {
                id
                name
            }
        }
        pageInfo {
            hasPreviousPage
            hasNextPage
            startCursor
            endCursor
        }
    }
}

The result of this query is a GraphQLResult that encodes to the following JSON:

{
    "people": {
        "edges": [
            {
                "cursor": "MQ==",
                "node": {
                    "id": 1,
                    "name": "Johnny Utah"
                }
            },
            {
                "cursor": "Mg==",
                "node": {
                    "id": 2,
                    "name": "Bodhi"
                }
            },
        ],
        "pageInfo": {
            "hasPreviousPage": false,
            "hasNextPage": false,
            "startCursor": "MQ==",
            "endCursor": "Mg=="
        }
    }
}

Federation

Federation allows you split your GraphQL API into smaller services and link them back together so clients see a single larger API. More information can be found here. To enable federation you must:

  1. Define Keys on the entity types, which specify the primary key fields and the resolver function used to load an entity from that key.
  2. Provide the schema SDL to the schema itself.

Here's an example for the following schema:

extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: [ "@extends", "@external", "@key", "@inaccessible", "@override", "@provides", "@requires", "@shareable", "@tag"])

type Product {
    id: ID!
    sku: String
    createdBy: User
}

extend type Query {
  product(id: ID!): Product
}

extend type User @key(fields: "email") {
  email: ID! @external
  name: String @override(from: "users")
  totalProductsCreated: Int @external
  yearsOfEmployment: Int! @external
}
import Foundation
import Graphiti
import NIO

struct Product: Codable {
    let id: String
    let sku: String
    let createdBy: User
}

struct User: Codable {
    let email: String
    let name: String?
    let totalProductsCreated: Int?
    let yearsOfEmployment: Int
}

struct ProductContext {
    func getUser(email: String) -> User { ... }
}

struct ProductResolver {
    struct UserArguments: Codable {
        let email: String
    }
    
    func user(context: ProductContext, arguments: UserArguments) -> User? {
        context.getUser(email: arguments.email)
    }
}

final class ProductSchema: PartialSchema<ProductResolver, ProductContext> {
    @TypeDefinitions
    override var types: Types {
        Type(Product.self) {
            Field("id", at: \.id)
            Field("sku", at: \.sku)
            Field("createdBy", at: \.createdBy)
        }
        
        Type(
            User.self,
            keys: {
                Key(at: ProductResolver.user) {
                    Argument("email", at: \.email)
                }
            }
        ) {
            Field("email", at: \.email)
            Field("name", at: \.name)
            Field("totalProductsCreated", at: \.totalProductsCreated)
            Field("yearsOfEmployment", at: \.yearsOfEmployment)
        }
    }
}

struct ProductAPI: API {
    let resolver: ProductResolver
    let schema: Schema<ProductResolver, ProductContext>
}

let schema = try SchemaBuilder(ProductResolver.self, ProductContext.self)
    .use(partials: [ProductSchema()])
    .setFederatedSDL(to: getSDL())
    .build()

let api = ProductAPI(resolver: ProductResolver(), schema: schema)
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

api.execute(
    request: """
    query {
      _entities(representations: {__typename: "User", email: "[email protected]"}) {
        ... on User {
          email
          name
        }
      }
    }
    """,
    context: ProductContext(),
    on: group
)