SwiftyGraphQL
这个库帮助编写更安全(类型安全)的 GraphQL 查询和变更,对于那些害怕生成代码的库的人来说。
要求
- Swift 5.3+
- iOS 11+, macOS 14+
安装
Swift Package Manager
创建一个 Package.swift
文件。
import PackageDescription
let package = Package(
name: "TestProject",
dependencies: [
.package(url: "https://github.com/hiimtmac/SwiftyGraphQL.git", from: "2.0.0")
]
)
Cocoapods
target 'MyApp' do
pod 'SwiftyGraphQL', '~> 2.0'
end
使用方法
一般使用方法如下
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
}
注意:输出将使用空格而不是换行符分隔
{ root: Root { sub { thing1 thing2 } thing3 thing4 ...object } } fragment object on Object { ... // object attributes }
别名
在.init(_ name: String, alias: String? = nil)
中添加节点别名。别名将改变返回json键对此节点的更改。
let node = GQLNode("sub") {
"thing1"
"thing2"
}
sub { thing1 thing2 }
let node = GQLNode("sub", alias: "cool") {
"thing1"
"thing2"
}
cool: sub { thing1 thing2 }
参数
待添加文档
变量
待添加文档
指令
当前支持以下指令
跳过指令
包含指令
片段
协议GQLFragmentable
可以使对象轻松地编码到查询或请求中。
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 }
提示:如果您的对象具有自定义
CodingKeys
实现,请将您的类型符合GQLCodable
以及其 CodingKeys 符合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 }
一旦您的对象符合 GQLFragmentable
,它就可以在查询中的 GQLFragment
对象中使用
let node = GQLNode("test") {
GQLFragment(Object.self)
Object.asFragment()
}
test { ...myobject }
fragment myobject on MyObject { name age }
查询
GQL
函数构建器对象用于创建 graphQL 查询(如上所述)。
示例
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 { ... }"
}
突变
GQLMutation
与 GQLQuery
类似。通常,您会包含用于突变的 operationName。
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 { ... }"
}
变量
向 GQL
添加变量。这将包括将参数包含在查询的头部,并在您将查询转换为 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
}
}
响应
已包括响应解码辅助程序。这不需要所有的返回结构都包含在对象上方的_levels_ key。
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
}
}
}
可以解码类似以下格式的响应
{
"data": {
{
"name": "taylor",
"age": 666
}
}
}
另外,如果解码无法完成,解码器将尝试解码 GQLErrors
,这是 graphQL 在您的查询/突变/模式中出错时返回的。 GQLErrors
符合 Error
,并由一系列 GQLError
组成。
示例
来自 graphQL 网站
GQL {
GQLNode("hero") {
"name"
}
}
query {
hero {
name
}
}
GQL {
GQLNode("hero") {
"name"
GQLNode("friends") {
"name"
}
}
}
query {
hero {
name
friends {
name
}
}
}
GQL {
GQLNode("human") {
"name"
GQLEmpty("height")
.withArgument("unit", value: "FOOT")
}
.withArgument("id", value: "1000")
}
query {
human(id: "1000") {
name
height(unit: "FOOT")
}
}
别名](https://graphql.net.cn/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
}
}
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
}
}
}
}
GQL(name: "HeroNameAndFriends") {
GQLNode("hero") {
"name"
GQLNode("friends") {
"name"
}
}
}
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
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"
}
// 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"
}
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
}
*/
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!"
}
}
行内片段 TODO: 实现
高级示例
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
}
}