low effort logo

GraphZahl (Alpha)

Swift Documentation

使用 Swift 创建最佳 GraphQL API。GraphZahl 是一个 Swift 框架,让您以最简单的方式编写服务器!它拥有神奇的、类似 Codable 的 API。

以下是您可能会喜欢它的主要原因

太长不看 (TL;DR)?

让我们直奔主题,编写我们的 Hello World!

// Create a GraphQLSchema
enum HelloWorld: GraphQLSchema {

    // Describe the Query Type 
    class Query: QueryType {
        func greeting(name: String) -> String {
            return "Hello, \(name)"
        }
    }
    
}

// Run a query using .perform
let result = HelloWorld.perform(request: "...")

您甚至可以将其连接到 GraphiQL 并进行测试

graphiql 左:数据查询 | 中:来自我们 API 返回的 json | 右:我们的服务器 API 参考

让我们分解一下!

每个 GraphQL API 都以根 Schema 开始,您在其中定义查询类型和突变类型

现在我们先关注查询类型。GraphZahl 现在将查看任何可以在 GraphQL 中提供的属性和函数,并将它们作为 API 中的字段提供。

在我们的示例中,这意味着它将找到函数 greeting,并看到所有输入和输出都可作为 GraphQL 类型使用并注册它。超级简单 😍!

安装

Swift Package Manager

您可以通过 Swift Package Manager 安装 GraphZahl,方法是将以下行添加到您的 Package.swift 文件中

import PackageDescription

let package = Package(
    [...]
    dependencies: [
        .package(url: "https://github.com/nerdsupremacist/GraphZahl.git", from: "0.1.0-alpha.")
        
        // It is recommended to use GraphZahl alongside Vapor
        .package(url: "https://github.com/nerdsupremacist/graphzahl-vapor-support.git", from: "0.1.0-alpha.")
    ]
)

用法

GraphZahl 的大多数用户需要了解六个主要提供的协议

以及一些扩展,这些扩展使您能够充分利用 GraphZahl 以及其他常见的服务器端库(如 Vapor 和 Fluent)。

GraphQLObject - 参考

您可以通过简单地让任何类实现 GraphQLObject 来提供它。

瞧 😍 !!!! 您不必实现任何东西。GraphZahl 将为您完成所有魔术

都将通过 GraphQL 提供。零麻烦。太棒了!!!

现在也可用了!就像那样。太棒了!!!

注意: 当涉及到 GraphQL 类型时,得益于条件一致性,GraphZahl 还开箱即用地支持 Optionals、Arrays 和 Futures。

让我们试试看

我们定义我们的类。带有一些属性和方法

class MyObject: GraphQLObject {
    let greeting = "Hello World!"
    let favouriteNumber = 42

    func count(to number: Int) -> String {
        return (0..<number).map(String.init).joined(separator: ", ")
    }
}

我们可以看到它出现在我们的 API 中

左:MyObject 的数据查询 | 中:来自我们 API 返回的 json | 右:MyObject 的类型定义

您的对象也可以返回嵌套对象

class OtherObject: GraphQLObject {
    let number: Int
    
    init(number: Int) {
        self.number = number
    }
}

class MyObject: GraphQLObject {
    ...
    
    let other = OtherObject(number: 1337)
    let others = [OtherObject(number: 0), OtherObject(number: 1)] // Arrays also work
}

您可以立即看到结果

GraphQLSchema - 参考

Schema 基本上是您在其中定义两个对象的命名空间:查询类型和突变类型。查询和突变的行为类似于常规的 GraphQLObject。上面提到的所有功能都将开箱即用。

QueryType 是强制性的,并且始终必须定义!如果您的 API 不需要突变,那么您就完成了。

如果您想使数据用户依赖怎么办?

这就是为什么查询和突变类型带有一个额外的约束(为了简单起见,这在之前的代码片段中被省略了)。它们有所谓的关联 ViewerContext,并且需要使用该 ViewerContext 进行初始化。注意:查询和突变的 ViewerContext 必须匹配。

例如,一个 Todo 应用程序可能看起来像这样

enum TodoApp: GraphQLSchema {
    typealias ViewerContext = LoggedInUser?

    class Query: QueryType {
        let user: LoggedInUser?
        
        func myTodos() -> [Todo]? {
            return user?.todosFromDB()
        }

        required init(viewerContext user: LoggedInUser?) {
            self.user = user
        }
    }
    
    class Mutation: MutationType {
        let user: LoggedInUser?
        
        func deleteTodo(id: UUID) -> Todo? {
            return user?.todos.find(id: id).delete()
        }

        required init(viewerContext user: LoggedInUser?) {
            self.user = user
        }
    }
}

如果您不需要 Viewer Context,只需将其设置为 Void

enum HelloWorld: GraphQLSchema {
    typealias ViewerContext = Void

    class Query: QueryType {
        func greeting(name: String) -> String {
            return "Hello, \(name)"
        }

        required init(viewerContext: Void) { }
    }
}

GraphQLScalar - 参考

如果您有一个可以表示为标准标量值的值,您也可以返回该值,并具有额外的类型安全优势,即不会将其与标准类型混合。

要实现 GraphQLScalar,您需要能够将其编码和解码为 ScalarValue(字符串、数字、布尔值)

例如,如果您想返回 URL,您可以在扩展中实现它

extension URL: GraphQLScalar {
    public init(scalar: ScalarValue) throws {
        // attempt to read a string and read a url from it
        guard let url = URL(string: try scalar.string()) else {
            throw ...
        }
        self = url
    }

    public func encodeScalar() throws -> ScalarValue {
        // delegate encoding to absolute string
        return try absoluteString.encodeScalar()
    }
}

搞定!每次对象中出现 URL 时,它都将可用

enum HelloWorld: GraphQLSchema {
    class Query: QueryType {
        let url = URL(string: "https://github.com/nerdsupremacist/GraphZahl")
    }
}

您几乎可以使用所有类型的类型来做到这一点:您选择的格式的日期、百分比、HTML 文本,任何您想要的。

GraphQLEnum - 参考

最后一个是最简单的情况。如果您想在 API 中支持枚举,它必须是 String 的 RawRepresentable 并实现 GraphQLEnum

如果您的枚举是 CaseIterable,那就完成了!

enum Envirornment: String, CaseIterable, GraphQLEnum {
    case production
    case development
    case testing
}

如果您想自己计算枚举 Cases,您可以实现 cases 函数。

GraphQLUnion - 参考

GraphZahl 支持联合类型。要实现联合类型,您只需实现一个枚举,其中每个 case 都有一个关联的类型,该类型是一个对象

enum SearchResult: GraphQLUnion {
    case user(User)
    case page(Page)
    case group(Group)
}

class Query: QueryType {
    func search(term: String) -> [SearchResult] {
        return [
            .user(user),
            .page(page),
            .group(group),
        ]
    }
}

GraphQLInputObject - 参考

如果您想将特定的结构体作为函数的参数,您可以使它们符合 GraphQLInputObject

enum Order: String, CaseIterable, GraphQLEnum {
    case ascending
    case descending
}

struct Options: GraphQLInputObject {
    let safeSearch: Bool
    let order: Order
}

class Query: QueryType {
    func search(term: String,
                arguments: Options = Options(safeSearch: true, order: .ascending)) -> [SearchResult] {
                
        return [...]
    }
}

子类支持

GraphZahl 支持子类化,但由于 GraphQL 中不提供子类化,因此它被抽象为一个额外的接口。

例如

class A: GraphQLObject {
    ...
}

class B: A {
    ...
}

将被表示为接口 A 和具体类型 __AB

// Interface that displays the output of any A
interface A {
    ...
}

// An instance of the superclass A
type __A implements A {
    ...
}

// An instance of the subclass B
type B implements A {
    ...
}

KeyPath 支持

如果您将 KeyPath 作为函数的参数,GraphZahl 将创建一个枚举,映射到所有具有相同类型的属性。

例如

class SearchResult: GraphQLObject {
    let relevance: Int
    let popularity: Int
    let name: String
}

class Schema: GraphQLSchema {
    class Query: QueryType {
        func search(term: String,
                    sortBy: KeyPath<SearchResult, Int>) -> [SearchResult] {

            return [SearchResult]().sort { $0[keyPath: sortBy] < $1[keyPath: sortBy] }
        }
    }
}

输出的定义是

type Query {
  search(sortBy: SearchResultField!, term: String!): [SearchResult!]!
}

type SearchResult {
  name: String!
  popularity: Int!
  relevance: Int!
}

enum SearchResultField {
  Relevance
  Popularity
}

扩展和插件

在 GraphZahl 之上还有一些扩展,用于添加对不同场景的支持,这些场景不一定是常态

Vapor 支持(推荐)

要通过 Vapor 提供您的 API,您可以使用 graphzahl-vapor-support

enum HelloWorld: GraphQLSchema {
    class Query: QueryType {
        func greeting(name: String) -> String {
            return "Hello, \(name)"
        }
    }
}

// Add the API to the Routes of your Vapor App
app.routes.graphql(path: "api", "graphql", use: HelloWorld.self)

您甚至可以添加 GraphiQL

app.routes.graphql(path: "api", "graphql", use: HelloWorld.self, includeGraphiQL: true)

Fluent 支持

要在您的 API 中使用 Fluent 类型和模型,您可以使用 graphzahl-fluent-support

enum API: GraphQLSchema {
    typealias ViewerContext = Database

    class Query: QueryType {
        let database: Database

        // QueryBuilders are supported with additional paging API
        func todos() -> QueryBuilder<Todo> {
            return Todo.query(on: database)
        }

        required init(viewerContext database: Database) {
            self.database = database
        }
    }
    
    ...
}

它增加了对以下内容的支持

部署

Heroku

如果您要部署到 Heroku,这非常简单。您需要 2 样东西

1. 添加构建包

将构建包添加到 heroku

heroku buildpacks:set nerdsupremacist/graph-zahl

2. 添加 Procfile

在我们的 Repo 中,我们将添加一个 Procfile,它将告诉 Heroku 我们应用程序的起点

例如,当我们的 API 的 Target 被称为 MyServer 并且正在使用 Vapor 时

web: MyServer serve --env production --hostname 0.0.0.0 --port $PORT

您还可以从 Vapor 的部署文档中获得一些灵感。

为 Linux 构建

如果您要为 Linux 构建 GraphZahl 应用程序,您需要添加 -E 链接器标志。例如

swift build -Xlinker -E {OTHER_FLAGS} -c debug

已知问题

贡献

欢迎并鼓励贡献!

相关工作

GraphZahl 与客户端的 Graphaello 结合使用效果最佳。Graphaello 使您能够直接从 SwiftUI 视图中使用 GraphQL。

了解更多

GraphZahl 以芝麻街的 Count von Count 命名,但在德语中是 “Graf Zahl”。

GraphZahl 在底层使用了 GraphQLSwiftRuntimeSwift NIO。如果您正在寻找替代方案,请查看 Graphiti,它使用起来更冗长和复杂,但为您提供更多控制和更好的性能。

目前这是一个研究项目。有关其工作原理的更多详细信息将在稍后发布。构建这个非常困难,所以相信我,我真的很想详细讨论它... 😉

许可证

GraphZahl 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。

本项目是在慕尼黑工业大学应用软件工程系的主管下完成的。该系拥有使用和维护此工具的永久权利。