├── Sources └── TinyGraphQL │ ├── OperationType.swift │ ├── QueryOperation.swift │ ├── Builder.swift │ ├── MutationOperation.swift │ ├── Extensions │ └── Encodable+Arguments.swift │ ├── Mutation.swift │ ├── Query.swift │ ├── ArgumentListRepresentable.swift │ ├── ArgumentRepresentable.swift │ ├── FieldRepresentable.swift │ ├── Operation.swift │ ├── GraphQL.swift │ └── Field.swift ├── Tests ├── LinuxMain.swift └── TinyGraphQLTests │ ├── XCTestManifests.swift │ └── TinyGraphQLTests.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .gitignore ├── .github └── workflows │ └── test.yml ├── LICENSE ├── Package.swift ├── TinyGraphQL.podspec └── README.md /Sources/TinyGraphQL/OperationType.swift: -------------------------------------------------------------------------------- 1 | public enum OperationType: String { 2 | case query 3 | case mutation 4 | } 5 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import TinyGraphQLTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += TinyGraphQLTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/QueryOperation.swift: -------------------------------------------------------------------------------- 1 | public protocol QueryOperation: Operation { 2 | 3 | } 4 | 5 | extension QueryOperation { 6 | public var type: OperationType { .query } 7 | } 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/TinyGraphQLTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(TinyGraphQLTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Builder.swift: -------------------------------------------------------------------------------- 1 | @_functionBuilder 2 | public final class Builder { 3 | public typealias Closure = () -> [T] 4 | public static func buildBlock(_ children: T...) -> [T] { 5 | children 6 | } 7 | } 8 | 9 | public typealias FieldBuilder = Builder 10 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/MutationOperation.swift: -------------------------------------------------------------------------------- 1 | protocol MutationOperation: Operation { 2 | 3 | } 4 | 5 | extension MutationOperation { 6 | public var type: OperationType { .mutation } 7 | 8 | public var description: String { 9 | return "mutation { \(parseBody()) }" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Extensions/Encodable+Arguments.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Encodable { 4 | var arguments: [String: Encodable & ArgumentRepresentable]? { 5 | guard let data = try? JSONEncoder().encode(self) else { return nil } 6 | return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { 7 | $0 as? [String: Any] 8 | }?.compactMapValues { $0 as? String }.mapValues { $0 as Encodable & ArgumentRepresentable } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Mutation.swift: -------------------------------------------------------------------------------- 1 | public struct Mutation: MutationOperation { 2 | public typealias Arguments = [String: ArgumentRepresentable] 3 | 4 | public var name: String 5 | public var arguments: Arguments 6 | public var fields: [Field] 7 | 8 | public init( 9 | _ name: String, 10 | _ arguments: Arguments, 11 | @FieldBuilder fields: Builder.Closure 12 | ) { 13 | self.name = name 14 | self.arguments = arguments 15 | self.fields = fields().map { $0.fieldRepresentation } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Query.swift: -------------------------------------------------------------------------------- 1 | public struct Query: QueryOperation { 2 | public typealias Arguments = [String: ArgumentRepresentable] 3 | 4 | public let name: String 5 | public let arguments: Arguments 6 | public let fields: [Field] 7 | 8 | public init( 9 | _ name: String, 10 | _ arguments: Arguments, 11 | @Builder fields: Builder.Closure 12 | ) { 13 | self.name = name 14 | self.arguments = arguments 15 | self.fields = fields().map { $0.fieldRepresentation } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/ArgumentListRepresentable.swift: -------------------------------------------------------------------------------- 1 | public protocol ArgumentListRepresentable { 2 | var argumentListRepresentation: [String: ArgumentRepresentable] { get } 3 | } 4 | 5 | extension Dictionary: ArgumentListRepresentable where Key == String, Value == ArgumentRepresentable { 6 | public var argumentListRepresentation: [String: ArgumentRepresentable] { 7 | return self 8 | } 9 | } 10 | 11 | extension Encodable where Self: ArgumentListRepresentable { 12 | public var argumentListRepresentation: [String: ArgumentRepresentable] { 13 | return self.arguments ?? [:] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/ArgumentRepresentable.swift: -------------------------------------------------------------------------------- 1 | public protocol ArgumentRepresentable { 2 | var argumentRepresentation: String { get } 3 | } 4 | 5 | extension String: ArgumentRepresentable { 6 | public var argumentRepresentation: String { 7 | return "\"\(self)\"" 8 | } 9 | } 10 | 11 | extension Dictionary: ArgumentRepresentable where Key == String, Value == String { 12 | public var argumentRepresentation: String { 13 | var result = "{" 14 | result += self.map { "\($0.key): \"\($0.value)\"" }.joined(separator: ", ") 15 | result += "}" 16 | return result 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/FieldRepresentable.swift: -------------------------------------------------------------------------------- 1 | public protocol FieldRepresentable: CustomStringConvertible { 2 | var fieldRepresentation: Field { get } 3 | } 4 | 5 | extension FieldRepresentable { 6 | var description: String { 7 | return fieldRepresentation.description 8 | } 9 | 10 | public var field: FieldRepresentable { 11 | return self 12 | } 13 | } 14 | 15 | extension Field: FieldRepresentable { 16 | public var fieldRepresentation: Field { 17 | return self 18 | } 19 | } 20 | 21 | extension String: FieldRepresentable { 22 | public var fieldRepresentation: Field { 23 | return Field(self) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | 7 | # OS X 8 | .DS_Store 9 | 10 | # Xcode 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata/ 21 | *.xccheckout 22 | profile 23 | *.moved-aside 24 | DerivedData 25 | *.hmap 26 | *.ipa 27 | 28 | # Bundler 29 | .bundle 30 | 31 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 32 | # Carthage/Checkouts 33 | 34 | Carthage/Build 35 | 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 39 | # 40 | # Note: if you ignore the Pods directory, make sure to uncomment 41 | # `pod install` in .travis.yml 42 | # 43 | # Pods/ 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | jobs: 8 | linux: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | image: 14 | - swift:5.3-xenial 15 | container: ${{ matrix.image }} 16 | env: 17 | LOG_LEVEL: info 18 | steps: 19 | - name: Checkout TinyGraphQL 20 | uses: actions/checkout@v2 21 | - name: Run base tests with Thread Sanitizer 22 | run: swift test --enable-test-discovery --sanitize=thread 23 | 24 | macOS_iOS: 25 | env: 26 | LOG_LEVEL: info 27 | runs-on: macos-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | - name: Build and Test (iOS) 32 | run: xcodebuild -scheme TinyGraphQL test -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 12' 33 | - name: Build and Test (macOS) 34 | run: swift test 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 cardoso 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "TinyGraphQL", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "TinyGraphQL", 12 | targets: ["TinyGraphQL"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "TinyGraphQL", 23 | dependencies: []), 24 | .testTarget( 25 | name: "TinyGraphQLTests", 26 | dependencies: ["TinyGraphQL"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Operation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol Operation: CustomStringConvertible { 4 | associatedtype Arguments: ArgumentListRepresentable 5 | 6 | var type: OperationType { get } 7 | var name: String { get } 8 | var arguments: Arguments { get } 9 | var fields: [Field] { get } 10 | 11 | var description: String { get } 12 | } 13 | 14 | extension Operation { 15 | public var description: String { 16 | parseBody() 17 | } 18 | 19 | func parseBody() -> String { 20 | var result = name 21 | 22 | let arguments = self.arguments.argumentListRepresentation 23 | 24 | if !arguments.isEmpty { 25 | result += "(" 26 | result += arguments.map { "\($0.key): \($0.value.argumentRepresentation)" }.joined(separator: ", ") 27 | result += ")" 28 | } 29 | 30 | let fields = self.fields.map { $0.field } 31 | 32 | if !fields.isEmpty { 33 | result += "{" 34 | result += fields.map { $0.description }.joined(separator: ", ") 35 | result += "}" 36 | } 37 | 38 | return result 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/GraphQL.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if canImport(FoundationNetworking) 3 | import FoundationNetworking 4 | #endif 5 | 6 | public struct GraphQL { 7 | public var url: URL 8 | public var headers: [String: String] 9 | 10 | public init(url: URL, headers: [String: String] = [:]) { 11 | self.url = url 12 | self.headers = headers 13 | } 14 | 15 | public func request(for operation: O) -> URLRequest { 16 | switch operation.type { 17 | case .query: 18 | let query = "?query={\(operation)}".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! 19 | let url = URL(string: "\(self.url.absoluteString)\(query)")! 20 | var urlRequest = URLRequest(url: url) 21 | urlRequest.allHTTPHeaderFields = headers 22 | return urlRequest 23 | case .mutation: 24 | var urlRequest = URLRequest(url: url) 25 | urlRequest.httpMethod = "POST" 26 | urlRequest.httpBody = """ 27 | { 28 | "query": "\(operation.description.replacingOccurrences(of: "\"", with: "\\\""))" 29 | } 30 | """.data(using: .utf8) 31 | urlRequest.allHTTPHeaderFields = headers 32 | return urlRequest 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/TinyGraphQL/Field.swift: -------------------------------------------------------------------------------- 1 | public struct Field { 2 | public var name: String 3 | public var arguments: [String: ArgumentRepresentable] 4 | public var fields: [Field] 5 | 6 | public var description: String { 7 | var result = name 8 | 9 | if !arguments.isEmpty { 10 | result += "(" 11 | result += arguments.map { "\($0.key): \"\($0.value)\"" }.joined(separator: ", ") 12 | result += ")" 13 | } 14 | 15 | if !fields.isEmpty { 16 | result += "{" 17 | result += fields.map { $0.description }.joined(separator: ", ") 18 | result += "}" 19 | } 20 | 21 | return result 22 | } 23 | 24 | public init( 25 | _ name: String, 26 | _ arguments: [String: ArgumentRepresentable] = [:], 27 | @FieldBuilder _ fields: FieldBuilder.Closure = { [] } 28 | ) { 29 | self.name = name 30 | self.arguments = arguments 31 | self.fields = fields().map { $0.fieldRepresentation } 32 | } 33 | 34 | init( 35 | name: String, 36 | arguments: [String: ArgumentRepresentable] = [:], 37 | fields: [Field] 38 | ) { 39 | self.name = name 40 | self.arguments = arguments 41 | self.fields = fields 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TinyGraphQL.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint TinyGraphQL.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'TinyGraphQL' 11 | s.version = '1.0.0' 12 | s.summary = 'A lightweight and easy-to-use GraphQL client for Swift' 13 | 14 | s.swift_version = '5.2' 15 | 16 | # This description is used to generate tags and improve search results. 17 | # * Think: What does it do? Why did you write it? What is the focus? 18 | # * Try to keep it short, snappy and to the point. 19 | # * Write the description between the DESC delimiters below. 20 | # * Finally, don't worry about the indent, CocoaPods strips it! 21 | 22 | s.description = <<-DESC 23 | A lightweight and easy-to-use GraphQL client for Swift 24 | - with 💘 from @GetStream 25 | DESC 26 | 27 | s.homepage = 'https://github.com/GetStream/TinyGraphQL' 28 | # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' 29 | s.license = { :type => 'MIT', :file => 'LICENSE' } 30 | s.author = { 'cardoso' => 'matheus@getstream.io' } 31 | s.source = { :git => 'https://github.com/GetStream/TinyGraphQL.git', :tag => s.version.to_s } 32 | s.social_media_url = 'https://twitter.com/getstream_io' 33 | 34 | s.ios.deployment_target = '11.0' 35 | 36 | s.source_files = 'Sources/TinyGraphQL/**/*' 37 | end 38 | -------------------------------------------------------------------------------- /Tests/TinyGraphQLTests/TinyGraphQLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import TinyGraphQL 3 | 4 | final class TinyGraphQLTests: XCTestCase { 5 | let graphQL = GraphQL( 6 | url: URL(string: "https://combase.app/graphql")!, 7 | headers: ["combase-organization": "orgid", "Content-Type": "application/json"] 8 | ) 9 | 10 | func testInit() { 11 | XCTAssertEqual(graphQL.url.absoluteString, "https://combase.app/graphql") 12 | XCTAssertEqual(graphQL.headers["combase-organization"], "orgid") 13 | XCTAssertEqual(graphQL.headers["Content-Type"], "application/json") 14 | } 15 | 16 | func testQuery() { 17 | let operation = Query("hello", ["hey": "sup"]) { 18 | "string_outside" 19 | Field("hey", ["hey": "sup"]) { 20 | Field("hey", ["lala": "lala"]) 21 | "string_inside_a_field" 22 | } 23 | } 24 | 25 | let request = graphQL.request(for: operation) 26 | 27 | XCTAssertEqual(request.httpMethod, "GET") 28 | 29 | guard let queryString = request.url?.query else { 30 | XCTFail("HTTP request url doesn't contain a query string") 31 | return 32 | } 33 | 34 | let expectedBody = "query=%7Bhello(hey:%20%22sup%22)%7Bstring_outside,%20hey(hey:%20%22sup%22)%7Bhey(lala:%20%22lala%22),%20string_inside_a_field%7D%7D%7D" 35 | 36 | XCTAssertEqual(queryString, expectedBody) 37 | } 38 | 39 | func testMutation() { 40 | let operation = Mutation("hello", ["hey": "sup"]) { 41 | "name" 42 | Field("hey", ["hey": "sup"]) { 43 | Field("hey", ["lala": "lala"]) 44 | } 45 | "byebye" 46 | } 47 | 48 | let request = graphQL.request(for: operation) 49 | 50 | XCTAssertEqual(request.httpMethod, "POST") 51 | 52 | guard 53 | let httpBody = request.httpBody, 54 | let bodyString = String(data: httpBody, encoding: .utf8) 55 | else { 56 | XCTFail("HTTP request doesn't contain a body") 57 | return 58 | } 59 | 60 | let expectedBody = "{\n \"query\": \"mutation { hello(hey: \\\"sup\\\"){name, hey(hey: \\\"sup\\\"){hey(lala: \\\"lala\\\")}, byebye} }\"\n}" 61 | 62 | XCTAssertEqual(bodyString, expectedBody) 63 | } 64 | 65 | func testQueryInheritance() { 66 | struct UserQuery: QueryOperation { 67 | struct Arguments: ArgumentListRepresentable, Encodable { 68 | let login: String 69 | } 70 | 71 | enum Fields: String { 72 | case name 73 | case id 74 | } 75 | 76 | var name: String = "user" 77 | var arguments: Arguments 78 | var fields: [Field] 79 | 80 | init(login: String, @Builder fields: Builder.Closure) { 81 | self.arguments = .init(login: login) 82 | self.fields = fields().map { $0.rawValue.fieldRepresentation } 83 | } 84 | } 85 | 86 | let operation = UserQuery(login: "cardoso") { 87 | UserQuery.Fields.name 88 | UserQuery.Fields.id 89 | } 90 | 91 | let request = graphQL.request(for: operation) 92 | 93 | XCTAssertEqual(request.httpMethod, "GET") 94 | 95 | guard let queryString = request.url?.query else { 96 | XCTFail("HTTP request url doesn't contain a query string") 97 | return 98 | } 99 | 100 | XCTAssertEqual(queryString, "query=%7Buser(login:%20%22cardoso%22)%7Bname,%20id%7D%7D") 101 | } 102 | 103 | static var allTests = [ 104 | ("testInit", testInit), 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![CI Status][ci-badge]][ci-url] 5 | [![Cocoapods][cocoapods-badge]][cocoapods-url] 6 | ![Cocoapods platforms](https://img.shields.io/cocoapods/p/Streamoji) 7 | ![SPM & Carthage compatible][spm-carthage-badge] 8 | [![License][license-badge]][license-url] 9 | [![Twitter Follow][follow-badge]][follow-url] 10 | [![Twitter][tweet-badge]][tweet-url] 11 | 12 | TinyGraphQL is a simple and lightweight query builder for the Swift language with zero dependencies. It provides a syntax close to that of GraphQL while preventing you from making time-consuming syntax mistakes. 13 | 14 | Made with 💘 by the folks @ [Stream](https://getstream.io). 15 | 16 | 17 | 18 | ## Table of Contents 19 | 20 | - [Snippets](#snippets) 21 | * [Initialization](#initialization) 22 | * [Query](#query) 23 | + [Raw GraphQL](#raw-graphql) 24 | + [TinyGraphQL](#tinygraphql) 25 | * [Mutation](#mutation) 26 | + [Raw GraphQL](#raw-graphql-1) 27 | + [TinyGraphQL](#tinygraphql-1) 28 | * [Request](#request) 29 | + [Raw GraphQL + URLSession](#raw-graphql---urlsession) 30 | + [TinyGraphQL + URLSession](#tinygraphql---urlsession) 31 | - [Installation](#installation) 32 | * [SPM](#spm) 33 | * [CocoaPods](#cocoapods) 34 | * [Carthage](#carthage) 35 | - [Adopters](#adopters) 36 | 37 | ## Snippets 38 | 39 | ### Initialization 40 | 41 | TinyGraphQL is also a container for pre-configuring your requests. In the initialization step, you should specify the URL for the GraphQL endpoint and any HTTP headers you'd need for the requests. 42 | 43 | ```swift 44 | let graphQL = TinyGraphQL( 45 | url: URL(string: "https://api.my.app/graphql")!, 46 | headers: ["Content-Type": "application/json", "combase-organization": "5fd7ecb251b33b10c380977b"] 47 | ) 48 | ``` 49 | 50 | ### Query 51 | 52 | See below a comparison between a regular GraphQL query and how to generate a similar query using TinyGraphQL. Note that it's possible to have multiple levels of fields like in regular GraphQL. 53 | 54 | #### Raw GraphQL 55 | 56 | ```graphql 57 | query { 58 | organizationById(_id: "(id)") { 59 | name 60 | stream { 61 | key 62 | } 63 | agentCount 64 | } 65 | } 66 | ``` 67 | 68 | #### TinyGraphQL 69 | 70 | ```swift 71 | Query("organizationById", ["_id": id]) { 72 | "name" 73 | Field("stream") { 74 | "key" 75 | } 76 | "agentCount" 77 | } 78 | ``` 79 | 80 | ### Mutation 81 | 82 | Mutations work pretty much the same as queries, except behind the scenes where it becomes a `POST` request instead of `GET`. 83 | 84 | #### Raw GraphQL 85 | 86 | ```graphql 87 | mutation { 88 | getOrCreateUser(record: { name: "(name)", email: "(email)" }) { 89 | _id 90 | name 91 | streamToken 92 | } 93 | } 94 | ``` 95 | 96 | #### TinyGraphQL 97 | 98 | ```swift 99 | Mutation("getOrCreateUser", ["record": ["name": name, "email": email]]) { 100 | "_id" 101 | "name" 102 | "streamToken" 103 | } 104 | ``` 105 | 106 | ### Request 107 | 108 | Making a request without TinyGraphQL is quite tedious and error prone. With TinyGraphQL, it's safe and easy. Look below at a comparison between building the request yourself and having TinyGraphQL do it for you. 109 | 110 | #### Raw GraphQL + URLSession 111 | 112 | ```swift 113 | var urlRequest = URLRequest(url: url) 114 | urlRequest.httpMethod = "POST" 115 | urlRequest.setValue("5fd7ecb251b33b10c380977b", forHTTPHeaderField: "combase-organization") 116 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 117 | urlRequest.httpBody = """ 118 | { 119 | "query": "mutation { createTicket(message: \\"\(message)\\", user: \\"\(userId)\\") { _id }}" 120 | } 121 | """.data(using: .utf8) 122 | 123 | URLSession.shared.dataTask(with: urlRequest, completionHandler: { data, response, error in 124 | // handle response 125 | } 126 | ``` 127 | 128 | #### TinyGraphQL + URLSession 129 | 130 | ```swift 131 | let query = Query("organizationById", ["_id": id]) { 132 | "name" 133 | Field("stream") { 134 | "key" 135 | } 136 | "agentCount" 137 | } 138 | 139 | let urlRequest = graphQL.request(for: query) 140 | 141 | URLSession.shared.dataTask(with: urlRequest, completionHandler: { data, response, error in 142 | // handle response 143 | } 144 | ``` 145 | 146 | ## Installation 147 | 148 | TinyGraphQL supports all three major dependency managers (SPM, CocoaPods, and Carthage) 149 | 150 | ### SPM 151 | 152 | ```swift 153 | .package(name: "TinyGraphQL", url: "https://github.com/getstream/TinyGraphQL", from: "0.0.2") 154 | ``` 155 | 156 | ### CocoaPods 157 | 158 | ```ruby 159 | pod 'TinyGraphQL', '~> 1.0' 160 | ``` 161 | 162 | ### Carthage 163 | 164 | ``` 165 | github "getstream/TinyGraphQL" ~> 1.0 166 | ``` 167 | 168 | ## Adopters 169 | 170 | - [Combase][combase-url]: Combase is an Open Source white-label chat solution that provides everything you need to instantly spin up a powerful, real-time customer support dashboard. TinyGraphQL is used by the [Combase Swift SDK][combase-swift-url]. 171 | 172 | If you use TinyGraphQL, you can show your support by [opening a PR](https://github.com/GetStream/TinyGraphQL/edit/main/README.md) and including your project or company in this list! 173 | 174 | [swift-badge]: https://img.shields.io/badge/Swift-5.2-orange.svg?style=flat 175 | [swift-url]: https://swift.org 176 | 177 | [ci-badge]: https://img.shields.io/github/workflow/status/getstream/tinygraphql/CI 178 | [ci-url]: https://github.com/GetStream/TinyGraphQL/actions?query=workflow%3ACI 179 | 180 | [cocoapods-badge]: https://img.shields.io/cocoapods/v/TinyGraphQL 181 | [cocoapods-url]: https://cocoapods.org/pods/TinyGraphQL 182 | 183 | [spm-carthage-badge]: https://img.shields.io/badge/SPM%20%26%20Carthage-compatible-green 184 | 185 | [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 186 | [license-url]: https://tldrlegal.com/license/mit-license 187 | 188 | [follow-badge]: https://img.shields.io/twitter/follow/getstream_io?style=social 189 | [follow-url]: https://twitter.com/intent/follow?screen_name=getstream_io 190 | 191 | [tweet-badge]: https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2FGetStream%2FStreamoji 192 | [tweet-url]: https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2FGetStream%2FTinyGraphQL 193 | 194 | [combase-url]: https://comba.se 195 | [combase-swift-url]: https://github.com/getstream/combase-swift 196 | --------------------------------------------------------------------------------