Build declarative GraphQL queries in Swift.

Overview

SociableWeaver

Swift meets GraphQL in this lightweight, easy to use framework. SociableWeaver uses a declarative style of programming and makes GraphQL queries look natural in Swift code. Through the use of Swift 5.1 function builders and CodingKeys, SociableWeaver removes all of the need for unsafe strings and Dictionaries when creating objects and fields.

Requirements

Xcode 11.x or a Swift 5.1x toolchain with Swift Package Manager.

Installation

Swift Package Manager

For projects using a .xcodeproj the best method is to navigate to File > Swift Packages > Add Package Dependency.... From there just simply enter https://github.com/NicholasBellucci/SociableWeaver as the package repository url and use the master branch or the most recent version. Master will always be inline with the newest release. The other method is to simply add .package(url: "https://github.com/NicholasBellucci/SociableWeaver.git", from: "0.1.0") to your Package.swift file's dependencies.

Carthage

Add the following entry to your Cartfile and run $ carthage update SociableWeaver

github "NicholasBellucci/SociableWeaver"

Table of Contents

Usage

SociableWeaver supports all that GraphQL has to offer. In order to get everything out of this framework, just make sure that any Codable models used contain CodingKeys. For example:

public struct Post: Codable {
    public enum CodingKeys: String, CodingKey, CaseIterable {
        case id, title, content
    }

    public let id: String
    public let title: String
    public let content: String

    public init(id: String, title: String, content: String) {
        self.id = id
        self.title = title
        self.content = content
    }
}

If CodingKeys aren't possible, SociableWeaver does support strings. It is highly recommended this be used as a last resort as it will make queries more difficult to manage.

Objects and Fields

GraphQL Fields

GraphQL is all about querying specific fields on objects and returning only what is needed. With SociableWeaver constructing objects with fields is a breeze.

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
}
GraphQL Query
query {
    post {
        id
        title
        content
    }
}

Arguments

GraphQL Arguments

Arguments are a key part of GraphQL and allow for much more refined queries. SociableWeaver supports arguments on both objects and fields.

The only requirement is that the value for the argument conforms to ArgumentValueRepresentable. Core types such as String, Int, Bool etc. will already conform. Enumerations will need to conform to the EnumValueRepresentable protocol.

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)

        Object(Post.CodingKeys.author) {
            Field(Author.CodingKeys.id)
            Field(Author.CodingKeys.name)
                .argument(key: "lastName", value: "Doe")
        }

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.recent)
    }
}
GraphQL Query
query {
    post {
        title
        author {
            id
            name(lastName: "Doe")
        }
        comments(filter: RECENT) {
            id
            content
        }
    }
}

Optionals

Optionals are supported and can be included in the query. In the instance where an optional should be included and the value is nil, the resulting GraphQL value will be null.

In order to include an optional make sure to get the argument value of the property without including a ?. This will result in a query param of age: null.

public struct Author: Codable {
    public enum CodingKeys: String, CodingKey, CaseIterable {
        case id, name, age, birthplace
    }

    ...
    public let age: Int?
    ...
}

extension Author: ArgumentValueRepresentable {
    public var argumentValue: String {
        var params: [String: String?] = [:]

        ...
        params["age"] = age.argumentValue
        ...

        let paramStrings: [String] = params.compactMap { argument in
            guard let value = argument.value else {
                return nil
            }

            return "\(argument.key): \(value)"
        }

        return "{ \(paramStrings.joined(separator: ",")) }"
    }
}'

Alias

GraphQL Alias

Aliases are key when querying a single object multiple times in the same request.

Swift
Weave(.query) {
    Object(Post.self) {
        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.recent)
        .alias("newComments")
        
        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.id)
            Field(Comment.CodingKeys.content)
        }
        .argument(key: "filter", value: CommentFilter.old)
        .alias("oldComments")
    }
}
GraphQL Query
query {
    post {
        newComments: comments(filter: RECENT) {
            id
            content
        }
        oldComments: comments(filter: OLD) {
            id
            content
        }
    }
}

Fragments

GraphQL Fragments

GraphQL fragments can help when building complicated queries. SociableWeaver makes them extremely simple and allows the proper references to be placed exactly where they would be in the query. With the help of a FragmentBuilder the FragmentReference can be added to the objects that require the fields and the Fragment can be added to the operation itself.

Swift
let authorFragment = FragmentBuilder(name: "authorFields", type: Author.self)
let query = Weave(.query) {
    Object(Post.self) {
        Object(Post.CodingKeys.author) {
            FragmentReference(for: authorFragment)
        }

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
            
            Object(Comment.CodingKeys.author) {
                FragmentReference(for: authorFragment)
            }
        }
    }

    Fragment(authorFragment) {
        Field(Author.CodingKeys.id)
        Field(Author.CodingKeys.name)
    }
}
GraphQL Query
query {
  post {
    author {
      ...authorFields
    }
    comments {
      content
      author {
        ...authorFields
      }
    }
  }
}

fragment authorFields on Author {
  id
  name
}

Operation Name

GraphQL Operation Name

Operation names aren't required but can make the queries more unique.

Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
}
.name("GetPostAndContent")
GraphQL Query
query GetPost {
  post {
    id
    title
    content
  }
}

Variables

GraphQL Variables

Since direct JSON is not needed when making queries in SociableWeaver, variables can and should be define in a method and passed into the query as arguments.

Swift
queryPost(id: 1)

func queryPost(id: Int) {
    Weave(.query) {
        Object(Post.self) {
            Field(Post.CodingKeys.title)
            Field(Post.CodingKeys.content)
            
            Object(Post.CodingKeys.author) {
                Field(Author.CodingKeys.id)
                Field(Author.CodingKeys.name)
            }
        }
        .argument(key: "id", value: id)
    }
}
GraphQL Query
query {
  post(id: 1) {
    title
    content
    author {
      id
      name
    }
  }
}

Directives

GraphQL Directives

Directives in GraphQL allows the server to affect execution of the query. The two directives are @include and @skip both of which can be added to fields or included fragments. The example defines true or false but in an actual query these values would be boolean variables.

Just to note, Skip will always take precedent over include. Also any objects/fragments that end up not having fields will be removed from the query.

let query = Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
            .include(if: true)

        Object(Post.CodingKeys.author) {
            Field(Author.CodingKeys.name)
        }
        .include(if: false)

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
                .include(if: true)
                .skip(if: true)
                
            Object(Comment.CodingKeys.author) {
                Field(Author.CodingKeys.name)
                    .skip(if: true)
            }
        }
    }
}
GraphQL Queries
query { 
    post { 
        title 
        content 
    } 
}

Mutations

GraphQL Mutations

Mutations work the same as simple queries and should be used when data is supposed to be written. An Object.schemaName will replace the name of the Object or Key included in the initializer.

Swift
Weave(.mutation) {
    Object(Post.self) {
        Field(Post.CodingKeys.id)
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)
    }
    .schemaName("createPost")
    .argument(key: "title", value: "TestPost")
    .argument(key: "content", value: "This is a test post.")
    .argument(key: "author", value: "John Doe")
}
GraphQL Mutation
mutation {
  createPost(title: "TestPost", content: "This is a test post.", author: "John Doe") {
    id
    title
    content
  }
}

Inline Fragments

GraphQL Inline Fragments

Inline fragments are useful when querying on an interface or union type as they allow the return of underlying types.

Swift
Weave(.query) {
    Object(Post.self) {
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)

        Object(Post.CodingKeys.comments) {
            Field(Comment.CodingKeys.content)
        
            Object(Comment.CodingKeys.author) {
                InlineFragment("AnonymousUser") {
                    Field(Author.CodingKeys.id)
                }

                InlineFragment("RegisteredUser") {
                    Field(Author.CodingKeys.id)
                    Field(Author.CodingKeys.name)
                }
            }
        }
    }
}
GraphQL Query
query {
  post {
    title
    content
    comments {
      content
      author {
        ... on AnonymousUser {
          id
        }
        ... on RegisteredUser {
          id
          name
        }
      }
    }
  }
}

Meta Fields

GraphQL Meta Fields

GraphQL meta fields can be customized and are recognized to have two proceeding underscores. The __typename meta field is a GraphQL default and can be used to return the object type in the results of a query.

Custom meta fields can be defined by using MetaFieldType.custom. This enum takes an associated String which does not need to include the double underscores before the name. For example: .custom("schema") results in __schema.

Swift
Weave(.query) {
    Object(Post.self){
        Field(Post.CodingKeys.title)
        Field(Post.CodingKeys.content)

        Object(Post.CodingKeys.author) {
            MetaField(.typename)
            Field(Author.CodingKeys.name)
        }
    }
}
GraphQL Query
query {
  post {
    title
    content
    author {
      __typename
      name
    }
  }
}

Pagination

GraphQL Pagination

SociableWeaver support pagination out of the box and can be easily customized. Features supported include slicing, edges, and page info inclusion.

Slicing

Slicing in GraphQL is great for fetching a specified amount of objects in a response. With SociableWeaver this can be specified with the Object.slice method.

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
}
GraphQL Query
{
  comments(first: 2) {
    id
    author
    content
  }
}

Cursor-Based Pagination

Cursor-based pagination is described as being the most powerful pagination type GraphQL provides. Setup this pagination by declaring the pagination type for an object.

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
    .paginationType(.cursor)
}
GraphQL Query
{
  comments(first: 2) {
    edges {
      cursor
      node {
        id
        author
        content
      }
    }
  }
}

Pagination Page Info

Including page info such as whether or not there is a next page or the end cursor is very flexible and supports a custom model.

Swift
Weave(.query) {
    Object(Post.CodingKeys.comments) {
        Field(Comment.CodingKeys.id)
        Field(Comment.CodingKeys.author)
        Field(Comment.CodingKeys.content)
    }
    .slice(amount: 2)
    .paginationType(.cursor)
    .pageInfo(type: PageInfo.self,
              keys: PageInfo.CodingKeys.startCursor,
                    PageInfo.CodingKeys.endCursor,
                    PageInfo.CodingKeys.hasNextPage)
}
GraphQL Query
{
  comments(first: 2) {
    edges {
      cursor
      node {
        id
        author
        content
      }
    }
    pageInfo {
      startCursor
      endCursor
      hasNextPage
    }
  }
}

Custom Types

SociableWeaver provides a couple of custom types that help to build more natural looking queries. These types may or may not have been included in examples but will also be defined in this section to provide more clarity.

ForEachWeavable

The ForEachWeavable struct was added for times where you may want to add objects or fields in a loop. More discussion around this use case can be found here.

Swift
let authors = [
    Author(id: "1", name: "John", age: 17, birthplace: [:]),
    Author(id: "2", name: "Jane", age: 29, birthplace: [:]),
    Author(id: "3", name: "Adam", age: 41, birthplace: [:])
]

let query = Weave(.query) {
    ForEachWeavable(authors) { author in
        Object("postsForAuthor") {
            Field(Author.CodingKeys.id)
            Field(Author.CodingKeys.name)
            Field(Author.CodingKeys.age)
            Field(Author.CodingKeys.birthplace)
        }
        .argument(key: "id", value: author.id)
    }
}
GraphQL Query
{
  postsForAuthor(id: "1") {
    id
    name
    age
    birthplace
  }
  postsForAuthor(id: "2") {
    id
    name
    age
    birthplace
  }
  postsForAuthor(id: "3") {
    id
    name
    age
    birthplace
  }
}

BuilderType

Due to current limitations with function builders, individual elements are not currently accepted. For that reason each function builder initializer has a corresponding initializer for a single element. BuilderType.individual has been set up to specify when an object or fragment will consist of only one element. The default value for the builderType parameter on all initializations is .individual. This means that passing it is not required and will result in the same outcome.

Object(Post.CodingKeys.author) {
    Field(Author.CodingKeys.name)
}

Fragment(authorFragment, .individual) {
    Field(Author.CodingKeys.name)
}

CaseStyleOption

This enumeration has been provided to allow for customization when it comes to object and fields that are initialized with a model or coding key. Defaulted to camel case.

Field(Comment.CodingKeys.createdAt)
    .caseStyle(.lowercase)

public enum CaseStyleOption {
    case lowercase
    case uppercase
    case capitalized
    case camelCase
    case pascalCase
    case snakeCase
    case kebabCase
}

EnumValueRepresentable

GraphQL enumeration values are represented as uppercase representations of the case names. For this reason, custom enumerations in swift that should be passed as argument values can conform to EnumValueRepresentable. This protocol conforms to ArgumentValueRepresentable and is extended to provide the argumentValue as an uppercase version of the case value.

enum PostCategories: EnumValueRepresentable {
    case art
    case music
    case technology
}


Object(Post.self) {
    ...
}
.argument(key: "category", value: PostCategories.technology)

/// Result: post(category: TECHNOLOGY) { ... }

License

SociableWeaver is, and always will be, MIT licensed. See LICENSE for details.

Comments
  • How to pass object array to Weave init in OperationBuilder parameter

    How to pass object array to Weave init in OperationBuilder parameter

    Screenshot 2020-11-07 at 16 30 52

    Hey there! Could you please tell me if its possible to build Weave query like this. I didn't find any approach here. Not so familiar with FunctionBuilders

    enhancement 
    opened by bududomasidet 6
  • How to use this library

    How to use this library

    I learned how to install it, and how to map my types to Graphql queries or mutations. But how those queries can be then used in my app? I have not seen any queries being generated in the filesystem in order to Graphql (Apollo) library can pick them up during build. Can you guide me please? If there is nothing yet of this kind, how can one manage it? Thanks

    opened by svoip 4
  • Not working as expected in Xcode 12 beta 6

    Not working as expected in Xcode 12 beta 6

    Hi!

    We're using SociableWeaver in our project and it has been great, however when building in Xcode 12 our project fails and when running the SociableWeaver tests, all but three tests fails.

    Version: v0.1.7 Xcode: 12.0 beta 6 (12A8189n)

    opened by westerlund 4
  • Add Carthage Support and Fix Compilation Error

    Add Carthage Support and Fix Compilation Error

    We're looking to implement this library in one of our apps at TKWW. Just had to make a few changes to support our use case and figured the public would benefit from these as well.

    • Added the Xcode project to git to enable Carthage support
    • Made the function builders public so that they can be used outside the library
    opened by evandcoleman 4
  • Pseudo graphql

    Pseudo graphql

    Hi, I'm given a subscription graphql url like this by the backend:

    curl --insecure -X POST https://someserver.io/app/v1/maintenance/subscribe
    
    returns:
    {
       "data":{
          "maintenance":{
             "message":"Maintenance message here",
             "isMaintenance":false
          }
       }
    }
    
    

    After which, I understand that it's a pseudo graphql implemented on their side.

    Is it possible to do this using SociableWeaver? I mean, just by giving the url without knowing the actual graphql endpoint and the query. I have not implemented any networking at all :)

    Thank you in advanced!

    opened by vinamelody 2
  • Ensure deterministic results

    Ensure deterministic results

    Before this change, if a dictionary is passed in as an argument value, it'd serialize in a random order. This sorts them and ensures all results will be deterministic. I updated a test case to illustrate the bug.

    opened by evandcoleman 2
  • addes basic optional support and support for nsnull in dictionary values

    addes basic optional support and support for nsnull in dictionary values

    Awesome stuff @NicholasBellucci ! One of the issues I have encountered with using your library is that it cannot support values that may be nil. One particular problem is that there is no way to send null values for keys in dictionaries.

    For more clarification, here is the use case: The API expects an attributes argument, which is a dictionary representing the fields on the object. Some of these fields may be nil/null. Typically, to avoid removing a key/value pair from a Swift dictionary, we will use NSNull() to nullify a field. This resulted in a completely empty field and graphQL parsing error instead of the null value we would expect. This work will change that.

    I also added some basic support for optional values, but if the wrapped type does not conform to ArgumentValueRepresentable it will have to default to null. This may not be the behavior you would expect but I think its worth adding so users of the library can have more support for nullifying fields.

    I also added a value of 1 for the Current Project Version build setting, without this you cannot include it in a application that is uploaded to the app store.

    opened by JSahli 1
  • Build issues with Xcode 14

    Build issues with Xcode 14

    Attempts to generate an xcframework via Carthage on Xcode 14 fail.

    System information: Monterey 12.6 on Apple M1 Pro Xcode 14.0.1 Carthage 0.38.0 SociableWeaver v 0.1.12

    Relevant information from the Carthage logs

    Users/username/Git/App/Carthage/Checkouts/SociableWeaver/Sources/SociableWeaver/FunctionBuilders/OperationBuilder.swift:19:44: error: cannot find type 'ForEachWeavable' in scope
                } else if let forEach = $0 as? ForEachWeavable {
                                               ^~~~~~~~~~~~~~~
    /Users/username/Git/App/Carthage/Checkouts/SociableWeaver/Sources/SociableWeaver/FunctionBuilders/OperationBuilder.swift:7:18: error: type of expression is ambiguous without more context
            children.forEach {
            ~~~~~~~~~^~~~~~~~~
    
    ** ARCHIVE FAILED **
    
    
    opened by kunal30xi 1
  • mutation with upload variable

    mutation with upload variable

    I wonder how to use Sociable Weaver to describe such information, I don't think this is currently possible but I would like to make sure, for now the only case I can't use your library is for my mutation that requires uploading a file.

    mutation MyOperation($preview: Upload, $file: Upload) {
        ...
    }
    

    Thanks.

    opened by quentinfasquel 1
Releases(0.1.12)
Owner
Nicholas Bellucci
iOS developer. Passionate about sharing code.
Nicholas Bellucci
Commands providing shortcuts to common Postgres introspection queries (Swift port of heroku-pg-extras)

Commands providing shortcuts to common Postgres introspection queries (Swift port of heroku-pg-extras)

Sven A. Schmidt 2 May 27, 2022
🔥 🔥 🔥Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

?? ?? ??Support for ORM operation,Customize the PQL syntax for quick queries,Support dynamic query,Secure thread protection mechanism,Support native operation,Support for XML configuration operations,Support compression, backup, porting MySQL, SQL Server operation,Support transaction operations.

null 60 Dec 12, 2022
Sharing SQL queries between Server and Mobile databases

Sharing SQL queries between Server and Mobile databases Overview As we all know, code is expensive to maintain, and the more complex the code, the mor

Aaron LaBeau 4 May 24, 2022
GraphQLite is a toolkit to work with GraphQL servers easily. It also provides several other features to make life easier during iOS application development.

What is this? GraphQLite is a toolkit to work with GraphQL servers easily. It also provides several other features to make life easier during iOS appl

Related Code 2.8k Jan 9, 2023
This project has been developed to understand GraphQL and Diffable Data Source. Created on 20.06.2022.

SpaceX Launches First in first. You need to build all packages before building the project. Packages: Extensions API Open Extensions folder under proj

Okan Yücel 3 Sep 29, 2022
A fast, pure swift MongoDB driver based on Swift NIO built for Server Side Swift

A fast, pure swift MongoDB driver based on Swift NIO built for Server Side Swift. It features a great API and a battle-tested core. Supporting both MongoDB in server and embedded environments.

null 646 Dec 10, 2022
SQLite.swift - A type-safe, Swift-language layer over SQLite3.

SQLite.swift provides compile-time confidence in SQL statement syntax and intent.

Stephen Celis 8.7k Jan 3, 2023
🧡 SQLiteOrm-Swift is an ORM library for SQLite3 built with Swift 5

?? Easy to use SQLite ORM library written with Swift

Yevgeniy Zakharov 25 Oct 6, 2022
ObjectBox Swift - persisting your Swift objects superfast and simple

ObjectBox Swift ObjectBox is a superfast, light-weight object persistence framework. This Swift API seamlessly persists objects on-device for iOS and

ObjectBox 380 Dec 19, 2022
Shows the issue with swift using an ObjC class which has a property from a swift package.

SwiftObjCSwiftTest Shows the issue with swift using an ObjC class which has a property from a swift package. The Swift class (created as @objc derived

Scott Little 0 Nov 8, 2021
Ios-App-ication-Swift - A simple iOS application made in Xcode using Swift

?? iPhone Calculator A simple iOS application made in Xcode using Swift. This ap

Kushal Shingote 1 Feb 2, 2022
Save-the-dot-project-swift - Save the dot project with swift

Save the Dot Apple introduced UIViewPropertyAnimator for iOS 10. We can use this

Kushal Shingote 2 Feb 8, 2022
The Swift Package Index is the place to find Swift packages!

The Swift Package Index helps you make better decisions about the dependencies you use in your apps. The Swift Package Index is a search engine for pa

Swift Package Index 389 Dec 22, 2022
A stand-alone Swift wrapper around the mongo-c client library, enabling access to MongoDB servers.

This package is deprecated in favour of the official Mongo Swift Driver. We advise users to switch to that pack

PerfectlySoft Inc. 54 Jul 9, 2022
Elegant library to manage the interactions between view and model in Swift

An assistant to manage the interactions between view and model ModelAssistant is a mediator between the view and model. This framework is tailored to

Seyed Samad Gholamzadeh 28 Jan 29, 2022
CRUD is an object-relational mapping (ORM) system for Swift 4+.

CRUD is an object-relational mapping (ORM) system for Swift 4+. CRUD takes Swift 4 Codable types and maps them to SQL database tables. CRUD can create tables based on Codable types and perform inserts and updates of objects in those tables. CRUD can also perform selects and joins of tables, all in a type-safe manner.

PerfectlySoft Inc. 61 Nov 18, 2022
CoreXLSX is a Excel spreadsheet (XLSX) format parser written in pure Swift

CoreXLSX Excel spreadsheet (XLSX) format parser written in pure Swift CoreXLSX is a library focused on representing the low-level structure of the XML

null 684 Dec 21, 2022
Solutions to LeetCode by Swift

LeetCode by Swift LeetCode Online Judge is a website containing many algorithm questions. Most of them are real interview questions of Google, Faceboo

Soap 4.5k Jan 5, 2023
Super lightweight DB written in Swift.

Use of value types is recommended and we define standard values, simple structured data, application state and etc. as struct or enum. Pencil makes us store these values more easily.

Naruki Chigira 88 Oct 22, 2022