◈ GraphQL

KNET oferuje wbudowaną obsługę GraphQL z DSL query builder i pełnym wsparciem dla mutations.

Szybki start

import rip.nerd.kitsunenet.graphql.KNETGraphQL

val graphql = KNETGraphQL("https://api.example.com/graphql")

// Proste query
val result = graphql.query("""
    query {
        users {
            id
            name
            email
        }
    }
""")

val users = result.data["users"]

Query z zmiennymi

val result = graphql.query(
    query = """
        query GetUser(${'$'}id: ID!) {
            user(id: ${'$'}id) {
                id
                name
                email
                posts {
                    title
                }
            }
        }
    """,
    variables = mapOf("id" to "123")
)

val user = result.data["user"] as Map<*, *>
println(user["name"])

Mutation

val result = graphql.mutation(
    mutation = """
        mutation CreateUser(${'$'}input: CreateUserInput!) {
            createUser(input: ${'$'}input) {
                id
                name
                email
            }
        }
    """,
    variables = mapOf(
        "input" to mapOf(
            "name" to "Jan Kowalski",
            "email" to "jan@example.com",
            "password" to "secret123"
        )
    )
)

if (result.hasErrors) {
    result.errors.forEach { error ->
        println("Error: ${error.message}")
    }
} else {
    val newUser = result.data["createUser"]
    println("Created user: $newUser")
}

DSL Query Builder

val query = graphqlQuery {
    operation("GetUsers") {
        field("users") {
            args("limit" to 10, "offset" to 0)

            field("id")
            field("name")
            field("email")

            field("profile") {
                field("avatar")
                field("bio")
            }

            field("posts") {
                args("status" to "PUBLISHED")
                field("id")
                field("title")
                field("createdAt")
            }
        }
    }
}

val result = graphql.execute(query)

DSL Mutation

val mutation = graphqlMutation {
    operation("CreatePost") {
        variables {
            variable("input", "CreatePostInput!")
        }

        field("createPost") {
            args("input" to "\$input")

            field("id")
            field("title")
            field("slug")
            field("author") {
                field("name")
            }
        }
    }
}

val result = graphql.execute(mutation, mapOf(
    "input" to mapOf(
        "title" to "My Post",
        "content" to "Content here..."
    )
))

Fragmenty

val query = graphqlQuery {
    fragment("UserFields", "User") {
        field("id")
        field("name")
        field("email")
    }

    operation("GetUsersAndMe") {
        field("users") {
            spread("UserFields")
        }

        field("me") {
            spread("UserFields")
            field("role")  // Dodatkowe pole
        }
    }
}

Obsługa błędów

val result = graphql.query(myQuery)

when {
    result.hasErrors && result.data == null -> {
        // Błąd krytyczny - brak danych
        result.errors.forEach { error ->
            Log.e("GraphQL", "Error: ${error.message}")
            Log.e("GraphQL", "Path: ${error.path}")
            Log.e("GraphQL", "Locations: ${error.locations}")
        }
        showError("Nie można pobrać danych")
    }

    result.hasErrors && result.data != null -> {
        // Częściowe dane z błędami
        Log.w("GraphQL", "Partial errors: ${result.errors}")
        processPartialData(result.data!!)
    }

    else -> {
        // Sukces
        processData(result.data!!)
    }
}

Typowana odpowiedź

data class UsersResponse(
    val users: List<User>
)

data class User(
    val id: String,
    val name: String,
    val email: String
)

val result = graphql.query<UsersResponse>("""
    query {
        users {
            id
            name
            email
        }
    }
""")

result.data?.users?.forEach { user ->
    println("${user.id}: ${user.name}")
}

Konfiguracja klienta

val graphql = KNETGraphQL.builder("https://api.example.com/graphql")
    .headers(mapOf(
        "Authorization" to "Bearer $token",
        "X-Client-Version" to "1.0.0"
    ))
    .timeout(30_000)
    .interceptor { request, chain ->
        // Custom logic
        chain(request)
    }
    .onError { errors ->
        // Global error handling
        analytics.logGraphQLErrors(errors)
    }
    .build()

Batching

// Batch multiple queries
val results = graphql.batch(
    graphqlQuery { field("users") { field("id"); field("name") } },
    graphqlQuery { field("posts") { field("id"); field("title") } },
    graphqlQuery { field("me") { field("id"); field("email") } }
)

val users = results[0].data["users"]
val posts = results[1].data["posts"]
val me = results[2].data["me"]

Przykład: Repository

class UserRepository(private val graphql: KNETGraphQL) {

    suspend fun getUsers(limit: Int = 20): List<User> {
        val result = graphql.query(
            """
            query GetUsers(${'$'}limit: Int!) {
                users(limit: ${'$'}limit) {
                    id
                    name
                    email
                    avatar
                }
            }
            """,
            mapOf("limit" to limit)
        )

        return result.data?.let {
            (it["users"] as List<*>).map { userData ->
                User.fromMap(userData as Map<*, *>)
            }
        } ?: emptyList()
    }

    suspend fun createUser(name: String, email: String): User? {
        val result = graphql.mutation(
            """
            mutation CreateUser(${'$'}name: String!, ${'$'}email: String!) {
                createUser(name: ${'$'}name, email: ${'$'}email) {
                    id
                    name
                    email
                }
            }
            """,
            mapOf("name" to name, "email" to email)
        )

        return result.data?.let {
            User.fromMap(it["createUser"] as Map<*, *>)
        }
    }
}

📚 Zobacz też