Swiftpack.co - Package - hiimtmac/SwiftyGraphQL

SwiftyGraphQL

Platform Swift Version

This library helps to make typesafe(er) graphql queries & mutations, for those who are scared of a library that does codegen

Requirements

  • Swift 5.3+
  • iOS 11+, macOS 14+

Installation

Swift Package Manager

Create a Package.swift file.

import PackageDescription

let package = Package(
  name: "TestProject",
  dependencies: [
    .package(url: "https://github.com/hiimtmac/SwiftyGraphQL.git", from: "2.1.0")
  ]
)

Cocoapods

target 'MyApp' do
  pod 'SwiftyGraphQL', '~> 2.1'
end

Usage

General usage looks as such

let query = GQL(name: "HeroNameAndFriends") {
    GQLNode("hero") {
        "name"
        "age"
        GQLNode("friends") {
            "name"
            "age"
        }
    }
    .withArgument("episode", variableName: "episode")
}
.withVariable("episode", value: "JEDI")
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    age
    friends {
      name
      age
    }
  }
}

GQLNode

let node = GQLNode("Root", alias: "root") {
    GQLNode("sub") {
        "thing1"
        "thing2"
    }
    "thing3"
    "thing4"
    GQLFragment(Object.self)
}
{
    root: Root {
        sub {
            thing1
            thing2
        }
        thing3
        thing4
        ...object
    }
}

fragment object on Object {
    ... // object attributes
}

Note: Output will be separated by spaces rather than carriage returns

{ root: Root { sub { thing1 thing2 } thing3 thing4 ...object } } fragment object on Object { ... // object attributes }

Alias

Add a node alias in .init(_ name: String, alias: String? = nil). An alias will change the returning json key for that node.

let node = GQLNode("sub") {
    "thing1"
    "thing2"
}
sub { thing1 thing2 }
let node = GQLNode("sub", alias: "cool") {
    "thing1"
    "thing2"
}
cool: sub { thing1 thing2 }

Arguments

TODO documentation

Variables

TODO documentation

Directives

Currently supported directives are:

  • SkipDirective
  • IncludeDirective

Fragment

The protocol GQLFragmentable can allow objects to easily be encoded into a query or request.

struct Object {
    let name: String
    let age: Int
}

extension Object: GQLFragmentable {
    // required
    static var graqhQl: GraphQLExpression {
        "name"
        "age"
    }
    // optional - will be synthesized from object Type name if not implemented
    static let fragmentName = "myobject"
    static var fragmentType = "MyObject"
}
...myobject
fragment myobject on MyObject { name age }

Tip: If your object has a custom CodingKeys implementation, conform your type to GQLCodable and its CodingKeys to CaseIterable

struct Object: GQLFragmentable, GQLCodable {
    let name: String
    let age: Int

    enum CodingKeys: String, CodingKey, CaseIterable {
        case name
        case age
    }
}
...object
fragment object on Object { name age }

Once your object conforms to GQLFragmentable, it can be used in a GQLFragment object in your query

let node = GQLNode("test") {
    GQLFragment(Object.self)
    Object.asFragment()
}
test { ...myobject }
fragment myobject on MyObject { name age }

Query

The GQL function builder object is used for creating the query for graphql (as seen above).

Example:

let query = GQL {
    GQLNode("allNodes") {
        GQLNode("frag") {
            Frag2.asFragment()
        }
        "hi"
        "ok"
    }
}

let json = try query.encode()
query {
    allNodes {
        frag {
            ...frag2
        }
        hi
        ok
    }
}

fragment frag2 on Frag2 {
    ...
}
*/
{
  "query": "{ query allNodes { frag { ...frag2 } hi ok } } fragment frag2 on Frag2 { ... }"
}

Mutation

GQLMutation is similar to the GQLQuery. Generally you would include the operationName for a mutation.

let mutation = GQL(.mutation, name: "NewPerson") {
    GQLNode("newPerson") {
        GQLNode("person", alias: "createdPerson") {
            GQLFragment(Frag2.self)
        }
    }
    .withArgument("id", value: "123")
    .withArgument("name", value: "taylor")
    .withArgument("age", value: 666)
}

let json = try query.encode()
mutation NewPerson {
    newPerson(id: "123", name: "taylor", age: 666) {
        createdPerson: person {
            ...frag2
        }
    }
}

fragment frag2 on Frag2 {
    ...
}
{
  "query": "mutation { newPerson(id: \"123\", name: \"taylor\", age: 666) { createdPerson: person { ...frag2 } } fragment frag2 on Frag2 { ... }"
}

Variables

Add variables to GQL. This will include the parameter in the head of the query as well as embed its contents into a dictionary to be serialized when you turn the query into json.

let query = GQL(name: "GetIt") {
    GQLNode("node") {
        "hello"
    }
    .withArgument("rating", variableName: "r")
}
.withVariable("rating", value: 5)

let json = try query.encode()
mutation GetIt($rating: Int) {
    node(rating: $r) {
        "hello"
    }
}
{
  "query": "mutation GetIt($rating: Int) { node(rating: $r) { hello } }",
  "variables": {
    "rating": 5
  }
}

Response

Helpers for response decoding have been included. This removes the requirement to make all return structs include the data key at a level up in the object.

extension JSONDecoder {
    public func graphQLDecode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
        do {
            return try decode(type, from: data)
        } catch {
            let graphQLError = try? JSONDecoder().decode(GQLErrors.self, from: data)
            throw graphQLError ?? error
        }
    }
}

Could decode a response that looks like:

{
    "data": {
        {
            "name": "taylor",
            "age": 666
        }
    }
}

Additionaly, if decoding cannot be completed, the decoder will try to decode a GQLErrors which graphql will return if there is an error in your query/mutation/schema. GQLErrors conforms to Error and is made up of an array of GQLError.

Examples

From the graphql website:

Fields

GQL {
    GQLNode("hero") {
        "name"
    }
}
query {
  hero {
    name
  }
}
GQL {
    GQLNode("hero") {
        "name"
        GQLNode("friends") {
            "name"
        }
    }
}
query {
  hero {
    name
    friends {
      name
    }
  }
}

Arguments

GQL {
    GQLNode("human") {
        "name"
        GQLEmpty("height")
            .withArgument("unit", value: "FOOT")
    }
    .withArgument("id", value: "1000")
}
query {
  human(id: "1000") {
    name
    height(unit: "FOOT")
  }
}

Aliases](https://graphql.org/learn/queries/#aliases)

GQL {
    GQLNode("hero", alias: "empireHero") {
        "name"
    }
    .withArgument("episode", value: "EMPIRE")
    GQLNode("hero", alias: "jediHero") {
        "name"
    }
    .withArgument("episode", value: "JEDI")
}
query {
  empireHero: hero(episode: "EMPIRE") {
    name
  }
  jediHero: hero(episode: "JEDI") {
    name
  }
}

Fragments

struct Character: GQLFragmentable {
    static let fragmentName = "comparisonFields"
    static var graqhQl: GraphQLExpression {
        "name"
        "appearsIn"
        GQLNode("friends") {
            "name"
        }
    }
}

GQL {
    GQLNode("hero", alias: "leftComparison") {
        GQLFragment(Character.self)
    }
    .withArgument("episode", value: "EMPIRE")
    GQLNode("hero", alias: "rightComparison") {
        GQLFragment(Character.self)
    }
    .withArgument("episode", value: "JEDI")
}
query {
  leftComparison: hero(episode: "EMPIRE") {
    ...comparisonFields
  }
  rightComparison: hero(episode: "JEDI") {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  appearsIn
  friends {
    name
  }
}
struct Character: GQLFragmentable {
    static let fragmentName = "comparisonFields"
    static var graqhQl: GraphQLExpression {
        "name"
        GQLNode("friendsConnection") {
            "totalCount"
            GQLNode("edges") {
                GQLNode("node") {
                    "name"
                }
            }
        }
        .withArgument("first", variableName: "first")
    }
}

let gql = GQL(name: "HeroComparison") {
    GQLNode("hero", alias: "leftComparison") {
        GQLFragment(Character.self)
    }
    .withArgument("episode", value: "EMPIRE")
    GQLNode("hero", alias: "rightComparison") {
        GQLFragment(Character.self)
    }
    .withArgument("episode", value: "JEDI")
}
query HeroComparison($first: Int) {
  leftComparison: hero(episode: "EMPIRE") {
    ...comparisonFields
  }
  rightComparison: hero(episode: "JEDI") {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}

Operation Names

GQL(name: "HeroNameAndFriends") {
    GQLNode("hero") {
        "name"
        GQLNode("friends") {
            "name"
        }
    }
}
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}

Variables

enum Episode: String, GraphQLVariableExpression, Codable {
    case jedi = "JEDI"
}

let gql = GQL(name: "HeroNameAndFriends") {
    GQLNode("hero") {
        "name"
        GQLNode("friends") {
            "name"
        }
    }
    .withArgument("episode", variableName: "episode")
}
.withVariable("episode", value: Episode.jedi as Episode?) // withouth `as Episode` it would output as `Episode!`
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

variables:
{
  "episode": "JEDI"
}

Default Variables

// this example doesnt make sense, you would just provide a default value with the optional
enum Episode: String, GraphQLVariableExpression, Codable {
    case jedi = "JEDI"
}

let optional: Episode? = nil

GQL(name: "HeroNameAndFriends") {
    GQLNode("hero") {
        "name"
    }
}
.withVariable("episode", value: optional ?? .jedi)
query HeroNameAndFriends($episode: Episode!) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}

variables:
{
  "episode": "JEDI"
}

Directives

enum Episode: String, GraphQLVariableExpression, Decodable {
    case jedi = "JEDI"
}

let withFriends = GQLVariable(name: "withFriends", value: false as Bool?)

let gql = GQL(name: "HeroNameAndFriends") {
    GQLNode("hero") {
        "name"
        GQLNode("friends") {
            "name"
        }
        .includeIf(withFriends)
    }
    .withArgument("episode", variableName: "episode")
}
.withVariable("episode", value: Episode.jedi as Episode?)
.withVariable(withFriends as GQLVariable)
query Hero($episode: Episode, $withFriends: Boolean) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}

variables:
{
  "episode": "JEDI",
  "withFriends": false
}
*/

Mutations

struct ReviewInput: GraphQLVariableExpression, Decodable {
    let stars: Int
    let commentary: String

    static var stub: Self { .init(stars: 5, commentary: "This is a great movie!") }
}

enum Episode: String, GraphQLVariableExpression, Decodable {
    case jedi = "JEDI"
}

let variable = GQLVariable(name: "ep", value: Episode.jedi)

let gql = GQL(.mutation, name: "CreateReviewForEpisode") {
    GQLNode("createReview") {
        "stars"
        "commentary"
    }
    .withArgument("episode", variable: variable)
    .withArgument("review", variableName: "review")
}
.withVariable(variable)
.withVariable("review", value: ReviewInput.stub)
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

variables:
{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

Inline Fragments TODO: implement

Advanced example

struct T1: GraphQLVariableExpression, Decodable, Equatable {
    let float: Float
    let int: Int
    let string: String
}

struct T2: GraphQLVariableExpression, Decodable, Equatable {
    let nested: NestedT2
    let temperature: Double
    let weather: String?

    struct NestedT2: Codable, Equatable {
        let name: String
        let active: Bool
    }
}

struct MyFragment: GQLFragmentable, GQLCodable {
    let p1: String
    let p2: String
    let p3: String

    enum CodingKeys: String, CodingKey, CaseIterable {
        case p1
        case p2
        case p3 = "hithere"
    }
}

let t1 = T1(float: 1.5, int: 1, string: "cool name")
let t2 = T2(nested: .init(name: "taylor", active: true), temperature: 2.5, weather: "pretty great")
let rev: String? = "this is great"

let gql = GQL(name: "MyCoolQuery") {
    GQLNode("first", alias: "realFirst") {
        "hello"
        "there"
        MyFragment.asFragment()
        GQLNode("inner") {
            GQLFragment(name: "adhoc", type: "MyFragment") {
                "p1"
                "p2"
            }
            GQLEmpty("cool")
                .skipIf("cool")
            GQLNode("supernested") {
                GQLFragment(MyFragment.self)
            }
            .withArgument("t2", variableName: "type2")
        }
        .withArgument("name", value: "taylor")
        .withArgument("age", value: 666)
        .withArgument("fraction", value: 2.59)
        .withArgument("rev", variableName: "review")
    }
    .withArgument("t1", variableName: "type1")
}
.withVariable("type1", value: t1)
.withVariable("type2", value: t2)
.withVariable("review", value: rev)
.withVariable("cool", value: true)
query MyCoolQuery($cool: Boolean!, $review: String, $type1: T1!, $type2: T2!) {
    realFirst: first(t1: $type1) {
        hello
        there
        ...myfragment
        inner(age: 666, fraction: 2.59, name: \"taylor\", rev: $review) {
            ...adhoc
            cool @skip(if: $cool)
            supernested(t2: $type2) {
                ...myfragment
            }
        }
    }
}

fragment adhoc on MyFragment { p1 p2 } fragment myfragment on MyFragment { p1 p2 hithere }
{
  "query": "query MyCoolQuery($cool: Boolean!, $review: String, $type1: T1!, $type2: T2!) { realFirst: first(t1: $type1) { hello there ...myfragment inner(age: 666, fraction: 2.59, name: \"taylor\", rev: $review) { ...adhoc cool @skip(if: $cool) supernested(t2: $type2) { ...myfragment } } } } fragment adhoc on MyFragment { p1 p2 } fragment myfragment on MyFragment { p1 p2 hithere }",
  "variables": {
    "type2": {
      "nested": { "name": "taylor", "active": true },
      "temperature": 2.5,
      "weather": "pretty great"
    },
    "type1": { "float": 1.5, "int": 1, "string": "cool name" },
    "review": "this is great",
    "cool": true
  }
}

Github

link
Stars: 1

Dependencies

Used By

Total: 0

Releases

Public Release - 2020-01-09 18:16:38

Function builder graphql! 🎉