Logo

TrailerQL

TrailerQL 是一个 Swift 包,它简化了查询 GraphQL 端点的许多步骤。

目前用于

用法

请参阅 TrailerQLTests.swift 以获取此处使用的完整代码

让我们看看瑞克和莫蒂现在在哪里...

let url = URL(string: "https://rickandmortyapi.com/graphql")!

让我们构建我们的查询模式。根据该站点的文档,我们想要创建这个 GraphQL 查询

characters(filter: { name: "Rick" }) {
    results {
        id
        name
        status
        location {
            id
            name
            type
        }
    }
}

在 TrailerQL 中,我们使用 GroupFieldFragmentBatchGroup 构建 GraphQL 对象关系。我们首先声明顶层组,并将其命名为 characters。我们还提供一个元组(或更多,如果需要),其中包含参数名称和值。在本例中,我们将整个 { name: "Rick" } 部分作为 filter 的参数值。

let schema = Group("characters", ("filter", "{ name: \"Rick\" }")) {
    Group("results") {
        Field.id
        Field("name")
        Field("status")
        Group("location") {
            Field.id
            Field("name")
            Field("type")
        }
    }
}

我们创建一个 Query,并将该模式指定为根元素。在本例中,我们还想禁用 GitHub 风格的速率检查,因为此 API 服务器不支持它。Query 初始化器有很多选项,因此请务必查看更详细的信息。

初始化器在 perNode 中接受一个闭包(或方法),当 TrailerQL 解析 API 响应数据时,会为解析的每个项目调用一次。我们将在下面看到如何做到这一点。

let query = Query(name: "Rick And Morty", rootElement: schema, checkRate: false, perNode: scanNode)

对它的每次调用都接受一个 Node 类型的参数。Node 包含已解析 GraphQL 对象的信息,例如其类型、其 ID 以及其父级的 ID(如果存在)。

TrailerQL 将不会解析不包含 ID 的节点,但它会很乐意“解包”层以查找其中的项目。例如,上面的“characters”组不是一个对象,而是一个容器,而“results”是包含 id 的对象列表。

private func scanNode(_ output: ParseOutput) {
    switch output {
    case .queryComplete:
        print("All nodes from the query received")

    case .queryPageComplete:
        print("All nodes from returned page of the query are received")

    case let .node(scannedNode):
        switch scannedNode.elementType {
        case "Character":
            if let newCharacter = Character(from: scannedNode) {
                Character.all.append(newCharacter)
            } else {
                print("Could not parse character from: \(scannedNode.jsonPayload)")
            }
        case "Location":
            if let newLocation = Location(from: scannedNode) {
                if let parentId = scannedNode.parent?.id,
                   let character = Character.find(id: parentId) {
                    character.location = newLocation
                }
            } else {
                print("Could not parse location from: \(scannedNode.jsonPayload)")
            }
        default:
            print("Unknown type: \(scannedNode.elementType)")
        }
    }
}

我们检查 scannedNode.elementType 以了解此回调是关于哪种类型的,然后使用它实例化每个对象。在内部,这些初始化器使用 Node.jsonPayload 属性来访问节点的 JSON 并从中反序列化一个实例。

queryCompletequeryPageComplete 可以提示任何处理这些节点的解析逻辑,这些逻辑可能需要知道何时首先传递了查询(或页面)的整个树。

每个 Character 在此端点的模式中都有一个关联的 Location。因此,既然我们已经创建了一个 Location,我们检查扫描的 Node.parent 属性,看看是否可以找到 Character 实例并将其添加到那里。

这个 Query 现在为我们生成了我们想要创建的 GraphQL 查询文本,在 query.queryText

let queryText = query.queryText

XCTAssert(queryText == " { characters(filter: { name: \"Rick\" }) { __typename results { __typename id name status location { __typename id name type } } } }")

让我们将此转换为 JSON 以发送到 API 端点,方法是将查询文本编码为键为“query”的 JSON 对象,并将其转换为字节。(任何 JSON 编码器都可以,在本例中我们使用的是默认的 Apple 框架)。

let dataToSend = try JSONSerialization.data(withJSONObject: ["query": queryText])

将其发布到 API,确保 API 知道这是 JSON

var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = dataToSend
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let result = try await URLSession.shared.data(for: request).0

让我们将响应解析为 JSON 对象...

let resultJson = try JSONSerialization.jsonObject(with: result)

...并将其馈送到 TrailerQL 中。processResponse 方法将运行并为它遇到的每个节点调用我们上面创建的回调。

_ = try await query.processResponse(from: resultJson)

在大型查询中,结果可能表明需要更多分页。如果发生这种情况,该方法将返回一个 Query 对象序列,可以运行这些对象来获取更多节点。

值得注意的是,运行这些查询然后可以返回更多 Query 对象,依此类推,只要有更多数据可用。

在这种情况下,TrailerQL 将尝试创建尽可能少的查询,但保持在 API 端点的最大节点限制之下。

让我们列出我们解析的任何 Character 对象!

for character in Character.all {
    print(character.id, character.description, separator: "\t")
}

片段

您可以将 GraphQL 片段与 Fragment 一起使用。例如,上面的查询可以写成

fragment locationFragment on Location { 
    id
    name
    type
}

fragment characterFragment on Character {
    id
    name
    status
}

{ 
    characters(filter: { name: "Rick" }) { 
        results { 
            ... characterFragment
            location { 
                ... locationFragment
            }
        }
    }
}

在 TrailerQL 中,这将表示为这样

let characterFragment = Fragment(on: "Character") {
    Field.id
    Field("name")
    Field("status")
}

let locationFragment = Fragment(on: "Location") {
    Field.id
    Field("name")
    Field("type")
}

let schema = Group("characters", ("filter", "{ name: \"Rick\" }")) {
    Group("results") {
        characterFragment
        Group("location") {
            locationFragment
        }
    }
}

在这个例子中,这实际上更复杂且不太有用,但对于我们需要在多个地方查询相同类型的情况,片段既提高了查询速度,也使模式更容易保持统一和可读。想象一下,如果有更多组期望 CharacterLocation,这将使事情变得多么好。

批处理

有时我们不想查询单个模式,而是想查询一堆相同类型的项目。BatchGroup 允许我们通过在一系列 Id 上 spread 一个片段来做到这一点。

假设我们要查询一堆角色的已知 ID。

fragment characterFragment on Character {
    id
    name
    status
    location {
        id
        name
        type
    }
}

{
    charactersByIds(ids: [1,2,3,4,5]) {
        ... characterFragment
    }
}

在 TrailerQL 上,上面将像这样生成...

let queries = Query.batching("Rick And Morty", groupName: "charactersByIds", idList: ["1", "2", "3", "4", "5"], checkRate: false, perNode: scanNode) {
    Fragment(on: "Character") {
        Field.id
        Field("name")
        Field("status")
        Group("location") {
            Field.id
            Field("name")
            Field("type")
        }
    }
}

...并像以前一样运行 Query。在这种情况下,根据 ID 列表的大小,可能会有多个查询,因为 TrailerQL 将尝试评估每个批次的节点成本,并使每个批次都低于节点限制(并记住每个查询可能都需要更多分页,具体取决于情况)

许可证

版权所有 (c) 2023 Paul Tsochantaris。根据 MIT 许可证获得许可,有关详细信息,请参阅 LICENSE。