Pytania rekrutacyjne GraphQL
38 pytań — 20 odpowiedzi dostępnych od razu
GraphQL - Podstawy i Koncepcje
Czym jest GraphQL i jakie problemy rozwiązuje w porównaniu do REST API?
Odpowiedź w 30 sekund: GraphQL to język zapytań dla API oraz runtime do wykonywania tych zapytań, stworzony przez Facebook w 2012 roku. Rozwiązuje problemy REST API takie jak over-fetching (pobieranie zbędnych danych), under-fetching (konieczność wielu requestów), oraz brak elastyczności w strukturze odpowiedzi.
Odpowiedź w 2 minuty: GraphQL to alternatywa dla REST API, która pozwala klientom precyzyjnie określić, jakie dane chcą pobrać w jednym zapytaniu. W przeciwieństwie do REST, gdzie każdy endpoint zwraca ustaloną strukturę danych, GraphQL umożliwia komponowanie zapytań według potrzeb klienta.
Główne problemy REST, które rozwiązuje GraphQL:
- Over-fetching - w REST często pobieramy więcej danych niż potrzebujemy, bo endpoint zwraca całą strukturę
- Under-fetching - potrzeba wykonania wielu requestów do różnych endpointów, aby zebrać wszystkie potrzebne dane
- Wersjonowanie API - GraphQL pozwala ewoluować API bez konieczności tworzenia nowych wersji
- Dokumentacja - schemat GraphQL jest self-documenting i można go automatycznie eksplorować
GraphQL używa jednego endpointu (zazwyczaj /graphql) i silnego typowania, co ułatwia development i debugging. Klient dokładnie wie, jakie dane otrzyma, a serwer może walidować zapytania przed wykonaniem.
Przykład kodu:
# REST - trzeba wykonać wiele requestów
# GET /users/123
# GET /users/123/posts
# GET /users/123/followers
# GraphQL - jeden request, dokładnie to czego potrzebujesz
query {
user(id: "123") {
name
email
posts {
title
createdAt
}
followers {
name
}
}
}
# Odpowiedź zawiera tylko te pola, które zażądaliśmy
graph LR
A[Klient] -->|Jedno zapytanie| B[GraphQL API]
B --> C[User Service]
B --> D[Post Service]
B --> E[Follower Service]
B -->|Jedna odpowiedź| A
style B fill:#e535ab
Materiały
Jakie są trzy główne operacje w GraphQL (Query, Mutation, Subscription)?
Odpowiedź w 30 sekund: GraphQL definiuje trzy typy operacji: Query (odczyt danych), Mutation (modyfikacja danych - tworzenie, aktualizacja, usuwanie), oraz Subscription (real-time subskrypcje zmian). Query i Mutation są synchroniczne, podczas gdy Subscription wykorzystuje WebSockety do asynchronicznej komunikacji.
Odpowiedź w 2 minuty: GraphQL operacje odpowiadają operacjom CRUD, ale w bardziej semantyczny sposób:
Query - służy do pobierania danych. Jest to operacja read-only, która nie powinna modyfikować stanu na serwerze. Queries mogą być wykonywane równolegle i są cachowalne. Podobnie jak GET w REST.
Mutation - służy do modyfikacji danych (tworzenie, aktualizacja, usuwanie). Mutations są wykonywane sekwencyjnie (jedna po drugiej) w kolejności, w jakiej zostały zdefiniowane w zapytaniu. Odpowiada POST, PUT, PATCH, DELETE w REST.
Subscription - służy do real-time komunikacji. Klient subskrybuje konkretne wydarzenia i otrzymuje aktualizacje przez WebSocket, gdy dane się zmieniają. Idealnie nadaje się do czatów, notyfikacji, live dashboardów. REST nie ma natywnego odpowiednika.
Wszystkie trzy operacje są definiowane w schemacie jako root types: Query, Mutation, i Subscription. Query jest wymagany, pozostałe opcjonalne.
Przykład kodu:
# QUERY - pobieranie danych
query GetUserProfile {
user(id: "123") {
name
email
posts {
title
}
}
}
# MUTATION - modyfikacja danych
mutation CreatePost {
createPost(input: {
title: "Mój nowy post"
content: "Treść posta..."
authorId: "123"
}) {
id
title
createdAt
author {
name
}
}
}
# SUBSCRIPTION - real-time aktualizacje
subscription OnNewPost {
postCreated {
id
title
author {
name
}
createdAt
}
}
# Wiele mutations wykonywanych sekwencyjnie
mutation MultipleOperations {
first: createPost(input: {...}) { id }
second: updateUser(id: "123", input: {...}) { id }
third: deleteComment(id: "456") { success }
}
# Wykonają się po kolei: first → second → third
sequenceDiagram
participant C as Klient
participant S as Serwer GraphQL
Note over C,S: Query - odczyt
C->>S: query { user(id: "123") }
S-->>C: { data: { user: {...} } }
Note over C,S: Mutation - zapis
C->>S: mutation { createPost(...) }
S->>S: Modyfikuje bazę danych
S-->>C: { data: { createPost: {...} } }
Note over C,S: Subscription - real-time
C->>S: subscription { postCreated }
Note over C,S: WebSocket połączenie
S-->>C: { data: { postCreated: {...} } }
S-->>C: { data: { postCreated: {...} } }
Materiały
Czym jest schemat GraphQL i jak definiuje kontrakt API?
Odpowiedź w 30 sekund: Schemat GraphQL to kontrakt między klientem a serwerem, definiujący wszystkie dostępne typy danych, operacje (queries, mutations, subscriptions) oraz relacje między nimi. Jest napisany w Schema Definition Language (SDL) i stanowi single source of truth dla całego API.
Odpowiedź w 2 minuty: Schemat GraphQL to typowany, self-documenting kontrakt API. Definiuje dokładnie, jakie dane można zapytać, jakie operacje są dostępne i jaką strukturę będą miały odpowiedzi. Schemat używa systemu typów, który jest walidowany podczas kompilacji i wykonania.
Główne elementy schematu:
- Typy obiektów - definiują strukturę danych (np. User, Post, Comment)
- Pola - właściwości obiektów z określonym typem
- Root types - Query, Mutation, Subscription - punkty wejścia do API
- Skalary - podstawowe typy (String, Int, Float, Boolean, ID)
- Enums - wyliczenia możliwych wartości
- Interfaces i Unions - abstrakcje dla polimorfizmu
- Input types - typy używane jako argumenty w mutations
Schemat zapewnia:
- Type safety - błędy typowania wykrywane przed wykonaniem
- Automatyczną dokumentację - narzędzia jak GraphiQL generują docs ze schematu
- Validację - każde zapytanie jest walidowane względem schematu
- Introspection - schemat można odpytać programatowo
Schemat jest niezależny od implementacji - ten sam schemat może być zaimplementowany w różnych językach programowania.
Przykład kodu:
# Definicja schematu w SDL (Schema Definition Language)
# Typy skalarne (wbudowane)
scalar DateTime
# Typ obiektowy
type User {
id: ID! # ! oznacza że pole jest wymagane (non-nullable)
name: String!
email: String!
age: Int
posts: [Post!]! # Tablica postów (nie może być null, elementy nie mogą być null)
role: UserRole!
createdAt: DateTime!
}
# Enum - zdefiniowany zestaw wartości
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
type Post {
id: ID!
title: String!
content: String!
author: User! # Relacja do User
comments: [Comment!]!
published: Boolean!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
# Input type - używany jako argument w mutations
input CreatePostInput {
title: String!
content: String!
authorId: ID!
published: Boolean = false # Wartość domyślna
}
# Root type Query - definiuje dostępne zapytania
type Query {
user(id: ID!): User
users(limit: Int = 10, role: UserRole): [User!]!
post(id: ID!): Post
posts(authorId: ID, published: Boolean): [Post!]!
}
# Root type Mutation - definiuje dostępne modyfikacje
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
createUser(name: String!, email: String!, role: UserRole!): User!
}
# Root type Subscription - definiuje real-time subskrypcje
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
# Interface - wspólny kontrakt dla różnych typów
interface Node {
id: ID!
createdAt: DateTime!
}
# Union - typ może być jednym z wielu typów
union SearchResult = User | Post | Comment
graph TD
A[Schemat GraphQL] --> B[Root Types]
A --> C[Object Types]
A --> D[Scalars & Enums]
A --> E[Input Types]
B --> B1[Query]
B --> B2[Mutation]
B --> B3[Subscription]
C --> C1[User]
C --> C2[Post]
C --> C3[Comment]
C1 -.relacja.-> C2
C2 -.relacja.-> C3
C3 -.relacja.-> C1
style A fill:#e535ab
style B fill:#4fb3d9
Materiały
Jak działa introspection w GraphQL i do czego służy?
Odpowiedź w 30 sekund:
Introspection to mechanizm pozwalający odpytać API GraphQL o jego własny schemat. Umożliwia to automatyczne generowanie dokumentacji, type-safety w klientach, oraz tworzenie narzędzi developerskich jak GraphiQL czy Apollo Studio. Jest dostępny przez specjalne zapytania do __schema i __type.
Odpowiedź w 2 minuty:
Introspection to jedna z najbardziej potężnych funkcji GraphQL - możliwość programowego zapytania API o jego strukturę, dostępne typy, pola, oraz operacje. Jest to zaimplementowane jako część standardu GraphQL i działa przez specjalne pola rozpoczynające się od __ (podwójne podkreślenie).
Główne zastosowania introspection:
- Automatyczna dokumentacja - narzędzia jak GraphiQL, GraphQL Playground, Apollo Studio wykorzystują introspection do generowania interaktywnej dokumentacji
- Type generation - generowanie typów TypeScript/Flow na podstawie schematu
- Walidacja zapytań - IDE i editory mogą walidować zapytania w czasie pisania
- Autocomplete - inteligentne podpowiedzi podczas pisania zapytań
- Schema stitching - łączenie wielu schematów GraphQL
Kluczowe pola introspection:
__schema- zwraca cały schemat z wszystkimi typami__type(name: String!)- zwraca informacje o konkretnym typie__typename- zwraca nazwę typu dla danego obiektu (dostępne w każdym zapytaniu)
Uwaga bezpieczeństwa: W środowisku produkcyjnym introspection często jest wyłączony dla publicznych API, aby nie ujawniać pełnej struktury schematu potencjalnym atakującym.
Przykład kodu:
# Pobranie wszystkich typów w schemacie
query IntrospectionQuery {
__schema {
types {
name
kind
description
}
}
}
# Szczegółowe informacje o konkretnym typie
query GetUserType {
__type(name: "User") {
name
kind
description
fields {
name
type {
name
kind
}
description
args {
name
type {
name
}
}
}
}
}
# Pobranie wszystkich dostępnych queries
query GetAllQueries {
__schema {
queryType {
name
fields {
name
description
type {
name
kind
}
args {
name
type {
name
kind
}
defaultValue
}
}
}
}
}
# Użycie __typename w zwykłym zapytaniu
query GetUserWithTypename {
user(id: "123") {
__typename # Zwróci "User"
id
name
posts {
__typename # Zwróci "Post"
title
}
}
}
# Pełne introspection query (używane przez narzędzia)
query FullIntrospection {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
// Przykład wykorzystania introspection w TypeScript
// Generowanie typów na podstawie schematu
import { getIntrospectionQuery, buildClientSchema } from 'graphql';
async function generateTypes() {
// 1. Pobranie schematu przez introspection
const introspectionQuery = getIntrospectionQuery();
const response = await fetch('http://api.example.com/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: introspectionQuery })
});
const { data } = await response.json();
// 2. Budowanie schematu z introspection data
const schema = buildClientSchema(data);
// 3. Schemat może być użyty do generowania typów TypeScript
// (np. przez graphql-code-generator)
console.log(schema.getQueryType());
}
sequenceDiagram
participant Dev as Developer
participant Tool as GraphiQL/IDE
participant API as GraphQL API
Dev->>Tool: Otwiera GraphiQL
Tool->>API: Wysyła introspection query
API-->>Tool: Zwraca pełny schemat
Tool->>Tool: Buduje dokumentację i autocomplete
Tool-->>Dev: Pokazuje dostępne typy i pola
Dev->>Tool: Pisze zapytanie
Tool->>Tool: Waliduje względem schematu
Tool-->>Dev: Podświetla błędy/sugestie
Materiały
Czym są fragmenty w GraphQL i kiedy ich używać?
Odpowiedź w 30 sekund:
Fragmenty to reużywalne jednostki pól w GraphQL, które pozwalają uniknąć duplikacji kodu w zapytaniach. Definiują się je słowem kluczowym fragment i używają przez spread operator .... Są szczególnie przydatne przy powtarzających się strukturach danych i dla type safety w aplikacjach z TypeScript.
Odpowiedź w 2 minuty: Fragmenty w GraphQL to mechanizm pozwalający wydzielić wspólne pola do reużywalnych bloków, podobnie jak funkcje w programowaniu. Zamiast powtarzać te same pola w wielu miejscach zapytania, definiujemy je raz jako fragment i używamy wielokrotnie.
Rodzaje fragmentów:
- Named Fragments - nazwane fragmenty, które można reużywać w wielu zapytaniach
- Inline Fragments - używane wewnątrz zapytań dla typów implementujących interface lub będących częścią union
Kiedy używać fragmentów:
- Gdy te same pola są zapytywane w wielu miejscach
- W komponentach React/Vue - każdy komponent definiuje fragment ze swoimi danymi (colocation pattern)
- Przy zapytaniach o typy polimorficzne (interfaces, unions)
- Dla lepszej organizacji kodu i DRY principle
- Przy generowaniu typów TypeScript - każdy fragment generuje osobny typ
Zalety:
- DRY - eliminacja duplikacji kodu
- Modularność - fragmenty można współdzielić między plikami
- Type safety - w TypeScript każdy fragment to osobny typ
- Colocation - komponenty definiują swoje wymagania danych
- Łatwiejsze refaktoryzacje - zmiana w jednym miejscu
Przykład kodu:
# Named Fragment - definicja
fragment UserBasicInfo on User {
id
name
email
avatar
}
fragment UserDetailedInfo on User {
...UserBasicInfo # Fragmenty mogą używać innych fragmentów
age
bio
location
createdAt
}
# Użycie fragmentów w zapytaniu
query GetUsers {
currentUser {
...UserDetailedInfo
}
recentUsers {
...UserBasicInfo
}
user(id: "123") {
...UserDetailedInfo
posts {
id
title
author {
...UserBasicInfo # Reużycie tego samego fragmentu
}
}
}
}
# Inline Fragments - dla typów polimorficznych
# Przykład z Union type
union SearchResult = User | Post | Comment
query Search($term: String!) {
search(term: $term) {
... on User {
id
name
email
}
... on Post {
id
title
content
author {
name
}
}
... on Comment {
id
text
author {
name
}
}
}
}
# Przykład z Interface
interface Node {
id: ID!
createdAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
name: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
title: String!
}
query GetNodes {
nodes {
id
createdAt
# Inline fragment dla specyficznych pól
... on User {
name
email
}
... on Post {
title
content
}
}
}
// Przykład użycia fragmentów w React z Apollo Client
import { gql, useQuery } from '@apollo/client';
// Fragment współdzielony przez komponenty
const USER_BASIC_INFO_FRAGMENT = gql`
fragment UserBasicInfo on User {
id
name
email
avatar
}
`;
// Komponent UserCard definiuje swoje wymagania
const UserCard = ({ userId }: { userId: string }) => {
const GET_USER = gql`
${USER_BASIC_INFO_FRAGMENT}
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicInfo
}
}
`;
const { data } = useQuery(GET_USER, { variables: { id: userId } });
return (
<div>
<img src={data?.user.avatar} alt={data?.user.name} />
<h3>{data?.user.name}</h3>
<p>{data?.user.email}</p>
</div>
);
};
// Komponent UserList używa tego samego fragmentu
const UserList = () => {
const GET_USERS = gql`
${USER_BASIC_INFO_FRAGMENT}
query GetUsers {
users {
...UserBasicInfo
}
}
`;
const { data } = useQuery(GET_USERS);
return (
<div>
{data?.users.map(user => (
<UserCard key={user.id} userId={user.id} />
))}
</div>
);
};
# Zaawansowany przykład - fragmenty warunkowe z dyrektywami
fragment UserInfo on User {
id
name
email @include(if: $includeEmail)
posts @skip(if: $skipPosts) {
id
title
}
}
query GetUser($id: ID!, $includeEmail: Boolean!, $skipPosts: Boolean!) {
user(id: $id) {
...UserInfo
}
}
# Fragmenty z różnymi poziomami szczegółowości
fragment UserMinimal on User {
id
name
}
fragment UserStandard on User {
...UserMinimal
email
avatar
}
fragment UserFull on User {
...UserStandard
age
bio
location
createdAt
posts {
id
title
}
followers {
...UserMinimal
}
}
graph TD
A[Query GetUsers] --> B[Fragment UserDetailedInfo]
A --> C[Fragment UserBasicInfo]
B --> C
D[Component UserCard] --> C
E[Component UserList] --> C
F[Component UserProfile] --> B
style C fill:#4fb3d9
style B fill:#e535ab
classDef fragment fill:#f9a825
Materiały
GraphQL - System Typów
Jakie typy skalarne są wbudowane w GraphQL?
Odpowiedź w 30 sekund:
GraphQL posiada pięć wbudowanych typów skalarnych: Int (liczby całkowite 32-bitowe), Float (liczby zmiennoprzecinkowe), String (ciągi znaków UTF-8), Boolean (wartości logiczne true/false) oraz ID (unikalny identyfikator serializowany jako string). Typy skalarne reprezentują liście drzewa zapytań - są najprostszymi jednostkami danych, które nie mogą być dalej rozwijane.
Odpowiedź w 2 minuty:
GraphQL definiuje pięć podstawowych typów skalarnych, które stanowią fundament systemu typów. Int reprezentuje liczby całkowite ze znakiem w zakresie 32-bitowym, idealne dla identyfikatorów liczbowych i liczników. Float obsługuje liczby zmiennoprzecinkowe podwójnej precyzji, używane do wartości dziesiętnych jak ceny czy współrzędne. String to sekwencje znaków UTF-8, wykorzystywane do tekstów, dat w formacie ISO czy JSON. Boolean przechowuje wartości logiczne true lub false, często używane w filtrach i flagach.
Typ ID jest specjalnym typem skalarnym zaprojektowanym dla unikalnych identyfikatorów. Chociaż jest serializowany jako string, sygnalizuje klientom i narzędziom, że wartość ta służy jako klucz i nie powinna być czytelna dla człowieka. GraphQL nie wymusza unikalności ID - to odpowiedzialność implementacji serwera.
Oprócz wbudowanych typów, można definiować własne typy skalarne (custom scalars) jak DateTime, URL, Email czy JSON. Wymagają one implementacji funkcji serializacji, deserializacji i walidacji po stronie serwera. Dzięki temu można enkapsulować logikę walidacji i zapewnić bezpieczeństwo typów dla specyficznych formatów danych.
Przykład kodu:
# Użycie wbudowanych typów skalarnych
type User {
id: ID! # Unikalny identyfikator (wymagany)
age: Int # Liczba całkowita (opcjonalna)
rating: Float # Liczba zmiennoprzecinkowa
name: String! # Ciąg znaków (wymagany)
isActive: Boolean # Wartość logiczna
}
# Definiowanie własnego typu skalarnego
scalar DateTime
scalar Email
scalar URL
type Post {
id: ID!
title: String!
createdAt: DateTime! # Własny skalar dla daty
authorEmail: Email! # Własny skalar z walidacją email
thumbnail: URL # Własny skalar dla adresów URL
}
# Zapytanie wykorzystujące typy skalarne
query {
user(id: "123") { # ID jako string
name # String
age # Int
rating # Float
isActive # Boolean
}
}
Materiały
Jak definiować własne typy obiektowe w schemacie GraphQL?
Odpowiedź w 30 sekund:
Typy obiektowe definiuje się używając słowa kluczowego type, po którym następuje nazwa typu i lista pól w nawiasach klamrowych. Każde pole ma nazwę i typ, opcjonalnie z argumentami. Znaki ! oznaczają pola wymagane (non-nullable), a nawiasy kwadratowe [] definiują listy. Typy obiektowe są podstawowymi blokami budulcowymi schematu GraphQL.
Odpowiedź w 2 minuty:
Typy obiektowe w GraphQL definiuje się za pomocą słowa kluczowego type, tworząc nazwany kontener dla pól. Każde pole składa się z nazwy, typu zwracanej wartości oraz opcjonalnych argumentów. Typ może być skalarny, inny typ obiektowy, enum, union lub interface. Modyfikatory typu określają jego kardinalność: brak modyfikatora oznacza wartość opcjonalną nullable, ! wymusza wartość non-nullable, a [Type] definiuje listę.
Pola w typie obiektowym mogą przyjmować argumenty, co umożliwia parametryzację zapytań bezpośrednio na poziomie pola. Argumenty działają podobnie jak parametry funkcji - mają nazwę, typ i opcjonalną wartość domyślną. Jest to potężna funkcjonalność pozwalająca na filtrowanie, paginację czy sortowanie danych bez potrzeby tworzenia dodatkowych zapytań.
GraphQL wspiera dokumentację bezpośrednio w schemacie poprzez komentarze docstring (potrójne cudzysłowy """). Dokumentacja ta jest automatycznie dostępna w narzędziach jak GraphiQL czy GraphQL Playground, co znacznie ułatwia eksplorację API. Dobrą praktyką jest opisywanie każdego typu i pola, szczególnie tych z nieoczywistą semantyką.
Przykład kodu:
# Dokumentacja typu - pojawi się w narzędziach introspection
"""
Reprezentuje użytkownika systemu z pełnym profilem i relacjami.
"""
type User {
# Unikalny identyfikator użytkownika (wymagany)
id: ID!
# Nazwa użytkownika (wymagana)
name: String!
# Adres email (opcjonalny)
email: String
# Wiek użytkownika (opcjonalny, liczba całkowita)
age: Int
# Lista postów użytkownika (wymagana lista, może być pusta)
# Parametr limit ogranicza liczbę zwracanych postów
posts(limit: Int = 10): [Post!]!
# Relacja many-to-many - lista przyjaciół
friends: [User!]!
# Pole z wieloma argumentami dla filtrowania
orders(
status: OrderStatus
minAmount: Float
sortBy: OrderSortField = CREATED_AT
): [Order!]!
}
"""
Post na blogu powiązany z autorem
"""
type Post {
id: ID!
title: String!
content: String!
# Relacja do typu User - pole może być null jeśli autor usunięty
author: User
# Lista tagów (wymagana lista, ale może być pusta)
tags: [String!]!
# Data publikacji (własny skalar)
publishedAt: DateTime
}
# Typ dla zamówień z zagnieżdżonymi obiektami
type Order {
id: ID!
total: Float!
status: OrderStatus!
items: [OrderItem!]!
customer: User!
}
type OrderItem {
id: ID!
quantity: Int!
price: Float!
product: Product!
}
type Product {
id: ID!
name: String!
price: Float!
inStock: Boolean!
}
Materiały
Czym są typy Input i jak różnią się od zwykłych typów obiektowych?
Odpowiedź w 30 sekund:
Typy Input (definiowane słowem kluczowym input) są specjalnymi typami obiektowymi używanymi wyłącznie jako argumenty w mutations i queries. W przeciwieństwie do zwykłych typów obiektowych, typy Input nie mogą mieć argumentów w polach, nie mogą implementować interfejsów ani zawierać referencji do zwykłych typów obiektowych - tylko do skalarów, enumów i innych typów Input. Służą do strukturyzowania złożonych danych wejściowych.
Odpowiedź w 2 minuty:
Typy Input w GraphQL rozwiązują problem przekazywania złożonych struktur danych jako argumentów do zapytań i mutacji. Definiowane są słowem kluczowym input zamiast type i mają ściśle ograniczoną funkcjonalność w porównaniu do zwykłych typów obiektowych. Ta separacja jest celowa i wynika z fundamentalnych różnic między danymi wejściowymi a wyjściowymi w GraphQL.
Kluczowe ograniczenia typów Input: pola nie mogą przyjmować argumentów (podczas gdy zwykłe typy mogą mieć parametryzowane pola), nie mogą implementować interfejsów ani być częścią unii, oraz mogą zawierać tylko skalary, enumy, listy i inne typy Input. Nie mogą zawierać referencji do zwykłych typów obiektowych. To oznacza, że graf danych wejściowych jest zawsze acykliczny i można go w pełni serializować.
Typy Input są niezbędne przy tworzeniu mutations z wieloma parametrami. Zamiast przekazywać dziesiątki pojedynczych argumentów, można zgrupować je w logiczne struktury Input. Znacznie poprawia to czytelność schematu, ułatwia walidację i umożliwia ponowne wykorzystanie tych samych struktur w różnych mutacjach. Konwencja nazewnictwa sugeruje dodawanie suffiksu Input do nazw tych typów (np. CreateUserInput, UpdatePostInput).
Ponieważ typy Input są używane do modyfikacji danych, często zawierają podzbiór pól z odpowiadających typów obiektowych. Na przykład UserInput może nie zawierać pola id (generowanego przez serwer) ani createdAt (automatycznego timestampu), ale zawiera wszystkie pola edytowalne przez użytkownika.
Przykład kodu:
# Zwykły typ obiektowy dla danych wyjściowych
type User {
id: ID!
username: String!
email: String!
age: Int
posts: [Post!]! # Może zawierać relacje do innych typów
createdAt: DateTime!
updatedAt: DateTime!
}
# Typ Input dla tworzenia użytkownika
input CreateUserInput {
username: String! # Tylko dane edytowalne przez klienta
email: String!
age: Int
# Brak: id, posts, createdAt, updatedAt - generowane przez serwer
}
# Typ Input dla aktualizacji (wszystkie pola opcjonalne)
input UpdateUserInput {
username: String
email: String
age: Int
}
# Zagnieżdżony typ Input dla złożonych struktur
input CreatePostInput {
title: String!
content: String!
tags: [String!]!
# Zagnieżdżony Input object
metadata: PostMetadataInput
}
input PostMetadataInput {
category: String
featured: Boolean
readTime: Int
}
# Mutation używająca typów Input
type Mutation {
# Zamiast wielu pojedynczych argumentów
createUser(input: CreateUserInput!): User!
# ID poza Input object - częsta praktyka
updateUser(id: ID!, input: UpdateUserInput!): User!
# Złożona mutacja z zagnieżdżonymi Inputs
createPost(input: CreatePostInput!): Post!
# Lista Input objects
createMultipleUsers(users: [CreateUserInput!]!): [User!]!
}
# Przykład użycia w mutation
# mutation {
# createUser(input: {
# username: "jan_kowalski"
# email: "jan@example.com"
# age: 25
# }) {
# id
# username
# email
# }
# }
# BŁĘDNE - Input NIE MOŻE zawierać referencji do zwykłych typów
# input InvalidInput {
# name: String!
# author: User! # ❌ BŁĄD - nie można używać typu obiektowego w Input
# }
# POPRAWNE - użyj ID dla referencji
input CreateCommentInput {
text: String!
postId: ID! # ✅ Referencja przez ID
authorId: ID! # ✅ Referencja przez ID
}
Materiały
Jak działają typy Enum i Union w GraphQL?
Odpowiedź w 30 sekund:
Enum to typ definiujący zamknięty zbiór możliwych wartości skalarnych (np. enum Status { ACTIVE, INACTIVE, PENDING }), używany do ograniczenia pola do określonych opcji. Union (union Result = User | Post | Comment) pozwala polu zwrócić jeden z kilku różnych typów obiektowych, wymagając użycia fragmentów inline lub named fragments w zapytaniu do określenia, które pola pobrać dla konkretnego typu.
Odpowiedź w 2 minuty:
Typy Enum reprezentują zamknięty zbiór możliwych wartości i są definiowane słowem kluczowym enum. Każda wartość enum jest identyfikatorem bez cudzysłowów (np. ACTIVE, nie "ACTIVE"). Enumy są serializowane jako stringi w JSON, ale GraphQL traktuje je jako odrębny typ, zapewniając walidację na poziomie schematu. Są idealne dla statusów, ról, kategorii czy innych pól z predefiniowanym zestawem opcji. Enumy mogą być używane zarówno w typach obiektowych, jak i Input, oraz jako argumenty zapytań.
Typy Union umożliwiają polu zwrócenie jednego z wielu możliwych typów obiektowych. Definiuje się je słowem kluczowym union i listą typów oddzielonych pionową kreską |. W przeciwieństwie do interfejsów, union nie wymaga wspólnych pól między typami - mogą być całkowicie różne. Jest to przydatne w scenariuszach search (wyniki mogą być różnymi typami), heterogenicznych list czy obsługi różnych typów błędów.
Podczas zapytania pola zwracającego union, klient musi użyć fragmentów inline (... on TypeName) lub named fragments, aby określić, które pola pobrać dla każdego możliwego typu. GraphQL zwraca pole __typename, które identyfikuje konkretny typ zwróconego obiektu. Pole __typename jest automatycznie dostępne w każdym typie obiektowym i jest kluczowe przy pracy z unions i interfejsami.
Różnica między Union a Interface: interfejsy wymagają wspólnych pól i relacji dziedziczenia, podczas gdy union jest prostym "albo/albo" bez wspólnej struktury. Union jest bardziej elastyczny, ale wymaga więcej kodu w zapytaniach. Nie można utworzyć union ze skalarów ani enumów - tylko z typów obiektowych.
Przykład kodu:
# ===== ENUM =====
# Definicja enum dla statusu użytkownika
enum UserStatus {
ACTIVE # Aktywny użytkownik
INACTIVE # Nieaktywny
SUSPENDED # Zawieszony
DELETED # Usunięty
}
# Enum dla ról
enum UserRole {
ADMIN
MODERATOR
USER
GUEST
}
# Enum dla sortowania
enum SortOrder {
ASC
DESC
}
# Użycie enumów w typach
type User {
id: ID!
name: String!
status: UserStatus! # Tylko wartości z enum UserStatus
role: UserRole! # Tylko wartości z enum UserRole
}
# Enum jako argument zapytania
type Query {
users(
status: UserStatus
role: UserRole
sortBy: SortOrder = ASC
): [User!]!
}
# Enum w Input types
input CreateUserInput {
name: String!
role: UserRole!
status: UserStatus = ACTIVE # Wartość domyślna
}
# ===== UNION =====
# Definicja union - wyniki wyszukiwania mogą być różnych typów
union SearchResult = User | Post | Comment | Tag
# Union dla różnych typów odpowiedzi (wzorzec Result)
union MutationResult = Success | ValidationError | NotFoundError
type Success {
message: String!
data: String
}
type ValidationError {
field: String!
message: String!
}
type NotFoundError {
resource: String!
id: ID!
}
# Użycie union w zapytaniu
type Query {
search(query: String!): [SearchResult!]!
# Różne typy mogą być zwrócone
feed: [FeedItem!]!
}
union FeedItem = Post | Ad | Notification
type Post {
id: ID!
title: String!
content: String!
}
type Ad {
id: ID!
imageUrl: String!
targetUrl: String!
}
type Notification {
id: ID!
message: String!
type: String!
}
# Zapytanie z fragmentami inline dla union
# query {
# search(query: "graphql") {
# __typename # Identyfikuje typ zwróconego obiektu
#
# ... on User {
# id
# name
# email
# }
#
# ... on Post {
# id
# title
# content
# }
#
# ... on Comment {
# id
# text
# author { name }
# }
# }
# }
# Named fragments dla reużywalności
# query SearchQuery {
# search(query: "test") {
# ...UserFields
# ...PostFields
# }
# }
#
# fragment UserFields on User {
# id
# name
# }
#
# fragment PostFields on Post {
# id
# title
# }
# Mutation z union result
type Mutation {
createUser(input: CreateUserInput!): MutationResult!
}
# Zapytanie mutation z obsługą różnych typów wyników
# mutation {
# createUser(input: { name: "Jan", role: ADMIN }) {
# __typename
# ... on Success {
# message
# data
# }
# ... on ValidationError {
# field
# message
# }
# ... on NotFoundError {
# resource
# id
# }
# }
# }
Materiały
Czym jest interfejs w GraphQL i kiedy go stosować?
Odpowiedź w 30 sekund:
Interfejs w GraphQL to abstrakcyjny typ definiujący wspólny zbiór pól, który mogą implementować inne typy obiektowe używając słowa kluczowego implements. Typy implementujące interfejs muszą zawierać wszystkie pola interfejsu i mogą dodawać własne. Interfejsy umożliwiają polimorfizm - pole może zwracać interfejs, a faktycznie otrzymać różne typy go implementujące, co wymaga użycia fragmentów w zapytaniach dla pól specyficznych dla typu.
Odpowiedź w 2 minuty:
Interfejs w GraphQL to abstrakcyjny typ służący do definiowania kontraktu - zestawu pól, które muszą być obecne w typach go implementujących. Definiuje się go słowem kluczowym interface, a typy obiektowe deklarują implementację używając implements InterfaceName. W przeciwieństwie do union, interfejs wymaga wspólnej struktury - wszystkie typy implementujące muszą posiadać co najmniej pola zdefiniowane w interfejsie, choć mogą dodawać własne dodatkowe pola.
Interfejsy są idealne w scenariuszach z hierarchią typów o wspólnej funkcjonalności: systemy z różnymi typami węzłów (Node interface z polem id),ContentManagement z różnymi typami treści (Article, Video, Podcast dzielące pola jak title, publishedAt), czy systemy komentarzy gdzie różne encje mogą być komentowane. GraphQL wspiera także implementację wielu interfejsów przez jeden typ (np. type Article implements Node & Timestamped & Searchable).
Podczas zapytania o pole zwracające interfejs, można pobierać pola zdefiniowane w interfejsie bezpośrednio, natomiast dla pól specyficznych dla konkretnych implementacji należy użyć fragmentów inline (... on TypeName). Podobnie jak przy unions, pole __typename jest automatycznie dostępne i pozwala klientowi rozróżnić typy implementujące.
Różnice między Interface a Union: interfejs wymaga wspólnych pól i reprezentuje relację "is-a" (Article jest Node), podczas gdy union to po prostu alternatywa typów bez wspólnej struktury. Interfejs jest lepszy gdy typy są naturalnie powiązane i dzielą funkcjonalność. Union jest bardziej elastyczny dla niepowiązanych typów. Interfejsy wspierają też ewolucję schematu - dodanie nowego pola do interfejsu automatycznie wymusza jego obecność we wszystkich implementacjach.
Przykład kodu:
# ===== PODSTAWOWY INTERFEJS =====
# Interfejs Node - wspólny wzorzec dla obiektów z ID
interface Node {
id: ID!
}
# Typy implementujące interfejs Node
type User implements Node {
id: ID! # Wymagane przez Node
name: String!
email: String!
# Dodatkowe pola specyficzne dla User
}
type Post implements Node {
id: ID! # Wymagane przez Node
title: String!
content: String!
author: User!
# Dodatkowe pola specyficzne dla Post
}
type Comment implements Node {
id: ID! # Wymagane przez Node
text: String!
author: User!
}
# ===== WIELOKROTNA IMPLEMENTACJA =====
# Interfejs dla obiektów z timestampami
interface Timestamped {
createdAt: DateTime!
updatedAt: DateTime!
}
# Interfejs dla obiektów wyszukiwalnych
interface Searchable {
searchText: String!
}
# Typ implementujący wiele interfejsów
type Article implements Node & Timestamped & Searchable {
# Pola z Node
id: ID!
# Pola z Timestamped
createdAt: DateTime!
updatedAt: DateTime!
# Pola z Searchable
searchText: String!
# Własne pola
title: String!
content: String!
author: User!
}
# ===== INTERFEJS DLA RÓŻNYCH TYPÓW TREŚCI =====
interface Content {
id: ID!
title: String!
description: String
publishedAt: DateTime
author: User!
tags: [String!]!
}
type BlogPost implements Content {
id: ID!
title: String!
description: String
publishedAt: DateTime
author: User!
tags: [String!]!
# Specyficzne dla BlogPost
content: String!
readTime: Int!
}
type Video implements Content {
id: ID!
title: String!
description: String
publishedAt: DateTime
author: User!
tags: [String!]!
# Specyficzne dla Video
url: String!
duration: Int!
thumbnail: String!
}
type Podcast implements Content {
id: ID!
title: String!
description: String
publishedAt: DateTime
author: User!
tags: [String!]!
# Specyficzne dla Podcast
audioUrl: String!
duration: Int!
episodeNumber: Int
}
# ===== UŻYCIE W QUERIES =====
type Query {
# Zwraca interfejs - może być dowolna implementacja
node(id: ID!): Node
# Lista różnych typów treści
content: [Content!]!
# Wyszukiwanie po interfejsie
searchContent(query: String!): [Searchable!]!
}
# Zapytanie pobierające wspólne pola interfejsu
# query {
# content {
# # Pola dostępne bezpośrednio z interfejsu Content
# id
# title
# publishedAt
# author {
# name
# }
#
# # Pola specyficzne dla konkretnych typów - wymagają fragmentów
# ... on BlogPost {
# content
# readTime
# }
#
# ... on Video {
# url
# duration
# thumbnail
# }
#
# ... on Podcast {
# audioUrl
# episodeNumber
# }
# }
# }
# Zapytanie z __typename dla rozróżnienia typów
# query {
# node(id: "123") {
# __typename # Zwraca "User", "Post", lub "Comment"
# id
#
# ... on User {
# name
# email
# }
#
# ... on Post {
# title
# content
# }
# }
# }
# ===== INTERFEJS Z ARGUMENTAMI W POLACH =====
interface Media {
id: ID!
title: String!
# Pole z argumentem w interfejsie
thumbnail(size: Int!): String!
}
type Image implements Media {
id: ID!
title: String!
# Musi implementować pole z tym samym argumentem
thumbnail(size: Int!): String!
width: Int!
height: Int!
}
type VideoFile implements Media {
id: ID!
title: String!
thumbnail(size: Int!): String!
duration: Int!
resolution: String!
}
Materiały
Jak obsługiwać wartości nullable i non-nullable w schemacie?
Odpowiedź w 30 sekund:
W GraphQL domyślnie wszystkie typy są nullable (mogą zwrócić null). Wykrzyknik ! po typie oznacza wartość non-nullable (wymaganą) - String! musi zwrócić string, nie może być null. Dla list: [String] to nullable lista nullable stringów, [String]! to wymagana lista nullable stringów, [String!] to nullable lista wymaganych stringów, a [String!]! to wymagana lista wymaganych stringów (najczęstsza kombinacja dla list).
Odpowiedź w 2 minuty:
System null-safety w GraphQL opiera się na prostej konwencji: typ bez modyfikatora może zwrócić null, natomiast typ z wykrzyknikiem ! musi zawsze zwrócić wartość. Jest to odwrotność wielu języków programowania, gdzie non-nullable jest domyślne. W GraphQL projektanci świadomie uczynili nullable wartością domyślną, uznając że w API dane często mogą być nieobecne (użytkownik bez avatara, opcjonalny opis produktu, itp.).
Dla typów skalarnych i obiektowych reguły są proste: String może być null lub stringiem, String! musi być stringiem. User może być null lub obiektem User, User! musi być obiektem. Jednak listy wprowadzają dodatkową złożoność, ponieważ zarówno sama lista, jak i jej elementy mogą być nullable lub non-nullable niezależnie. [String] - lista może być null, elementy mogą być null. [String]! - lista nigdy nie jest null (może być pusta), ale elementy mogą być null. [String!] - lista może być null, ale jeśli istnieje, wszystkie elementy muszą być stringami. [String!]! - lista nigdy nie jest null i wszystkie elementy muszą być stringami (najpopularniejsza kombinacja dla list).
Walidacja null odbywa się na poziomie runtime podczas wykonywania zapytania. Jeśli pole non-nullable zwróci null, GraphQL propaguje błąd w górę do najbliższego nullable pola lub do data: null na poziomie root, zwracając szczegóły w sekcji errors. To zachowanie chroni spójność danych - klient otrzymuje albo kompletne dane zgodne ze schematem, albo wyraźny sygnał błędu.
Przy projektowaniu schematu należy rozważyć trade-offy: zbyt wiele pól non-nullable czyni API kruche (każdy błąd nulluje całe zapytanie), ale zbyt wiele nullable wymusza obsługę null w każdym kliencie. Dobra praktyka: pola identyfikujące (id) i kluczowe dane domeny powinny być non-nullable, natomiast pola opcjonalne, relacje które mogą być usunięte, i pola podlegające błędom zewnętrznych serwisów powinny być nullable.
Przykład kodu:
# ===== PODSTAWOWE TYPY NULLABLE/NON-NULLABLE =====
type User {
# Non-nullable - zawsze zwraca wartość
id: ID!
username: String!
# Nullable - może zwrócić null
email: String # Użytkownik może nie mieć emaila
avatar: String # Avatar jest opcjonalny
bio: String # Bio może nie być wypełnione
age: Int # Wiek jest opcjonalny
# Non-nullable z wartością domyślną to dobra praktyka
isActive: Boolean!
}
# ===== LISTY - KOMBINACJE NULLABLE =====
type Post {
id: ID!
title: String!
# [String] - lista może być null, elementy mogą być null
# Możliwe wartości: null, [], ["tag1", null, "tag2"]
tags: [String]
# [String]! - lista nigdy nie jest null (może być pusta), elementy mogą być null
# Możliwe wartości: [], ["tag1"], ["tag1", null, "tag2"]
# NIEMOŻLIWE: null
categories: [String]!
# [String!] - lista może być null, ale elementy NIE mogą być null
# Możliwe wartości: null, [], ["tag1", "tag2"]
# NIEMOŻLIWE: ["tag1", null]
keywords: [String!]
# [String!]! - lista nigdy nie jest null, elementy NIE mogą być null
# Możliwe wartości: [], ["tag1", "tag2"]
# NIEMOŻLIWE: null, ["tag1", null]
# To jest NAJCZĘSTSZA kombinacja dla list
relatedTopics: [String!]!
# Listy obiektów - te same zasady
comments: [Comment!]! # Wymagana lista wymaganych komentarzy
likes: [Like] # Opcjonalna lista opcjonalnych like'ów
}
# ===== RELACJE I NULLABLE =====
type Comment {
id: ID!
text: String!
# Autor może być null jeśli konto zostało usunięte
author: User
# Post musi istnieć - komentarz bez posta nie ma sensu
post: Post!
# Odpowiedzi opcjonalne - lista może być pusta ale nie null
replies: [Comment!]!
}
# ===== ARGUMENTS I NULLABLE =====
type Query {
# Argument non-nullable - musi być podany
user(id: ID!): User
# Argumenty nullable - opcjonalne filtry
users(
status: UserStatus
role: UserRole
limit: Int
): [User!]!
# Mix - ID wymagane, reszta opcjonalna
posts(
authorId: ID!
tag: String
published: Boolean
): [Post!]!
}
# ===== INPUT TYPES I NULLABLE =====
input CreateUserInput {
# Pola wymagane przy tworzeniu
username: String!
email: String!
# Pola opcjonalne
bio: String
avatar: String
}
input UpdateUserInput {
# Wszystkie pola opcjonalne - partial update
username: String
email: String
bio: String
avatar: String
}
# ===== PROPAGACJA BŁĘDÓW NULL =====
type Article {
id: ID!
title: String!
# Jeśli author jest non-nullable i zwróci null,
# cały obiekt Article będzie null
author: User!
# Jeśli coAuthor jest nullable i zwróci null,
# tylko to pole będzie null, reszta Article zostanie
coAuthor: User
}
# Przykład odpowiedzi z błędem propagacji null:
# {
# "data": {
# "article": null // Całość null bo author! zwrócił null
# },
# "errors": [{
# "message": "Cannot return null for non-nullable field Article.author",
# "path": ["article", "author"]
# }]
# }
# ===== DOBRE PRAKTYKI =====
type Product {
# Identyfikatory zawsze non-nullable
id: ID!
sku: String!
# Główne dane domeny non-nullable
name: String!
price: Float!
# Opcjonalne dane dodatkowe nullable
description: String
image: String
# Listy zwykle non-nullable (pusta lista zamiast null)
images: [String!]!
variants: [ProductVariant!]!
# Relacje do usuwalnych encji nullable
category: Category # Kategoria może być usunięta
# Wartości boolean z domyślną wartością non-nullable
inStock: Boolean!
featured: Boolean!
}
# ===== ZAGNIEŻDŻONE NON-NULLABLE =====
type Order {
id: ID!
# Lista wymagana, elementy wymagane, pola w elementach wymagane
items: [OrderItem!]!
}
type OrderItem {
id: ID!
quantity: Int!
# Product musi istnieć - OrderItem bez Product to błąd danych
product: Product!
}
# Przykład zapytania pokazującego różne kombinacje:
# query {
# user(id: "123") {
# id # String! - zawsze zwróci wartość
# username # String! - zawsze zwróci wartość
# email # String - może być null
# posts { # [Post!]! - zawsze lista, nigdy pusta pozycja
# title # String! - zawsze
# tags # [String] - może być null lub zawierać nulle
# categories # [String]! - zawsze lista, może zawierać nulle
# keywords # [String!] - może być null, elementy nie
# relatedTopics # [String!]! - zawsze lista, elementy zawsze
# }
# }
# }
Materiały
GraphQL - Queries i Mutations
Jak konstruować zapytania z zagnieżdżonymi polami w GraphQL?
Odpowiedź w 30 sekund: Zagnieżdżone pola w GraphQL pozwalają na pobieranie powiązanych danych w jednym zapytaniu. Struktura zapytania odzwierciedla kształt zwracanych danych - każde pole może zawierać własne pod-pola, tworząc hierarchię. GraphQL automatycznie rozwiązuje relacje między obiektami, eliminując problem N+1 queries przy odpowiedniej implementacji resolverów.
Odpowiedź w 2 minuty: Zapytania z zagnieżdżonymi polami są jedną z najważniejszych cech GraphQL, pozwalającą na pobieranie powiązanych danych w pojedynczym żądaniu. Struktura zapytania jest hierarchiczna - każdy obiekt może zawierać pola będące innymi obiektami, które również mogą być rozwijane. Klient precyzyjnie określa, jakie dane i na jakiej głębokości chce otrzymać.
GraphQL automatycznie wykonuje wszystkie niezbędne operacje bazodanowe lub wywołania serwisów, aby dostarczyć kompletne dane zgodnie z zapytaniem. Serwer używa resolverów dla każdego pola, które są wykonywane hierarchicznie - najpierw resolver głównego pola, potem resolvery jego pod-pól. Pozwala to na optymalizację zapytań poprzez techniki takie jak DataLoader, który grupuje i deduplikuje zapytania.
Głębokość zagnieżdżenia jest nieograniczona teoretycznie, ale w praktyce powinna być kontrolowana przez mechanizmy zabezpieczeń po stronie serwera (query depth limiting, query complexity analysis). Zagnieżdżone zapytania mogą również zawierać argumenty na każdym poziomie, co daje dużą elastyczność w filtrowaniu i parametryzacji danych.
Kluczową zaletą jest możliwość pobrania wszystkich potrzebnych danych w jednym round-trip do serwera, zamiast wykonywania wielu kolejnych zapytań (jak w REST API). Kształt otrzymanych danych dokładnie odpowiada strukturze zapytania, co czyni GraphQL bardzo przewidywalnym i łatwym w użyciu.
Przykład kodu:
# Proste zapytanie z zagnieżdżonymi polami
query {
user(id: "123") {
name
email
# Zagnieżdżone pole - lista postów użytkownika
posts {
id
title
# Kolejny poziom zagnieżdżenia - komentarze do posta
comments {
id
content
author {
name
avatar
}
}
}
# Zagnieżdżone pole - profil użytkownika
profile {
bio
website
# Dane adresowe w profilu
address {
city
country
}
}
}
}
# Przykład z argumentami na różnych poziomach
query {
organization(id: "456") {
name
# Filtrowanie na poziomie zagnieżdżonym
repositories(first: 10, orderBy: {field: CREATED_AT, direction: DESC}) {
name
starCount
# Paginacja na głębszym poziomie
issues(first: 5, state: OPEN) {
title
createdAt
assignees(first: 3) {
login
avatarUrl
}
}
}
}
}
# Kompleksowy przykład z wieloma poziomami
query GetUserDashboard {
currentUser {
id
username
# Statystyki użytkownika
stats {
postCount
followerCount
followingCount
}
# Lista obserwujących z ich danymi
followers(limit: 20) {
id
username
avatar
# Sprawdzamy czy my też ich obserwujemy
isFollowedByMe
}
# Tablica wątków z najnowszymi wiadomościami
conversations(unreadOnly: true) {
id
participants {
username
avatar
lastSeen
}
lastMessage {
content
sentAt
sender {
username
}
# Informacja o załącznikach
attachments {
type
url
filename
}
}
}
}
}
Materiały
Czym są argumenty w zapytaniach GraphQL i jak ich używać?
Odpowiedź w 30 sekund: Argumenty w GraphQL to parametry przekazywane do pól lub zapytań, pozwalające na filtrowanie, sortowanie, paginację i modyfikację zwracanych danych. Każde pole może przyjmować własne argumenty zdefiniowane w schemacie. Argumenty mogą być wartościami literalnymi lub zmiennymi, co umożliwia dynamiczne konstruowanie zapytań.
Odpowiedź w 2 minuty: Argumenty są fundamentalnym mechanizmem parametryzacji zapytań w GraphQL. W przeciwieństwie do REST API, gdzie parametry są przekazywane w URL lub body, GraphQL pozwala przekazywać argumenty bezpośrednio do każdego pola w zapytaniu. Schemat definiuje, jakie argumenty są dostępne dla każdego pola, ich typy oraz czy są wymagane.
Argumenty mogą być używane do wielu celów: filtrowania danych (np. status: "active"), paginacji (first: 10, after: "cursor123"), sortowania (orderBy: "createdAt"), wyszukiwania (search: "phrase") czy przekazywania identyfikatorów (id: "123"). Każde pole w hierarchii zapytania może mieć własne, niezależne argumenty, co daje ogromną elastyczność.
Argumenty mogą być przekazywane na dwa sposoby: jako wartości literalne bezpośrednio w zapytaniu lub jako zmienne. Zmienne są preferowane w aplikacjach produkcyjnych, ponieważ umożliwiają ponowne użycie tego samego zapytania z różnymi wartościami oraz zwiększają bezpieczeństwo (zapobiegają injection attacks). W schemacie argumenty są definiowane z typami, wartościami domyślnymi i walidacją.
Argumenty są również kluczowe w mutations, gdzie służą do przekazywania danych do utworzenia, aktualizacji lub usunięcia. Często stosuje się wzorzec "input type", gdzie wszystkie dane dla mutacji są grupowane w jeden obiekt wejściowy, co ułatwia walidację i dokumentację.
Przykład kodu:
# Podstawowe użycie argumentów - wartości literalne
query {
user(id: "123") {
name
posts(limit: 5, status: PUBLISHED) {
title
}
}
}
# Argumenty z różnymi typami danych
query {
products(
category: "electronics"
priceRange: { min: 100, max: 1000 }
inStock: true
tags: ["laptop", "gaming"]
) {
name
price
}
}
# Paginacja z argumentami
query {
allPosts(
first: 10 # Liczba elementów
after: "cursor123" # Kursor po którym zacząć
orderBy: CREATED_AT # Sortowanie
direction: DESC # Kierunek sortowania
) {
edges {
node {
title
createdAt
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
# Używanie zmiennych zamiast wartości literalnych
query GetUser($userId: ID!, $postLimit: Int = 10) {
user(id: $userId) {
name
posts(limit: $postLimit, status: PUBLISHED) {
title
publishedAt
}
}
}
# Zmienne przekazane osobno:
# {
# "userId": "123",
# "postLimit": 20
# }
# Argumenty na różnych poziomach zagnieżdżenia
query SearchContent($searchTerm: String!, $resultCount: Int!) {
search(query: $searchTerm, type: ALL) {
users(first: $resultCount) {
username
posts(limit: 3, hasMedia: true) {
title
mediaUrl
}
}
posts(first: $resultCount, sortBy: RELEVANCE) {
title
author {
username
}
comments(first: 2, orderBy: LIKES) {
content
likesCount
}
}
}
}
# Argumenty typu Input dla złożonych danych
mutation CreateProduct($input: CreateProductInput!) {
createProduct(input: $input) {
product {
id
name
price
}
errors {
field
message
}
}
}
# Zmienne:
# {
# "input": {
# "name": "Laptop",
# "price": 1299.99,
# "category": "electronics",
# "tags": ["laptop", "computer"],
# "specifications": {
# "ram": "16GB",
# "storage": "512GB SSD"
# }
# }
# }
# Argumenty z wartościami domyślnymi w schemacie
query GetPosts($status: PostStatus = PUBLISHED, $limit: Int = 20) {
posts(status: $status, limit: $limit) {
title
status
}
}
# Filtrowanie z enumami i obiektami
query FilterUsers($filter: UserFilter!) {
users(filter: $filter) {
id
username
email
}
}
# Zmienne:
# {
# "filter": {
# "role": "ADMIN",
# "isActive": true,
# "createdAfter": "2024-01-01",
# "searchTerm": "john"
# }
# }
Materiały
Jak definiować i używać zmiennych w operacjach GraphQL?
Odpowiedź w 30 sekund: Zmienne w GraphQL pozwalają na parametryzację zapytań bez konieczności modyfikowania ich treści. Są definiowane w sygnaturze operacji z typem i opcjonalną wartością domyślną, a następnie używane poprzez znak $ w ciele zapytania. Zmienne są przekazywane osobno od zapytania jako obiekt JSON, co zwiększa bezpieczeństwo i umożliwia cache'owanie zapytań.
Odpowiedź w 2 minuty: Zmienne są kluczowym mechanizmem w GraphQL, który oddziela logikę zapytania od jego danych wejściowych. Zamiast wstawiać wartości bezpośrednio do zapytania (inline), zmienne są definiowane w sygnaturze operacji i przekazywane osobno jako JSON. To podejście ma wiele zalet: umożliwia ponowne użycie tego samego zapytania z różnymi wartościami, ułatwia walidację i typowanie, zwiększa bezpieczeństwo (zapobiega injection attacks) oraz pozwala na cache'owanie skompilowanych zapytań.
Zmienne są definiowane w nawiasach po nazwie operacji, używając składni $nazwaZmiennej: Typ. Typ może być skalarny (String, Int, Boolean, Float, ID), enum, input type lub ich tablica. Wykrzyknik (!) oznacza, że zmienna jest wymagana. Można również określić wartość domyślną używając składni $zmienna: Typ = wartość. W ciele zapytania zmienne są używane poprzez odniesienie do ich nazwy poprzedzonej znakiem $.
GraphQL wykonuje walidację zmiennych przed wykonaniem zapytania, sprawdzając czy wszystkie wymagane zmienne są przekazane i czy ich typy odpowiadają definicji w schemacie. To zapewnia type safety i wczesne wykrywanie błędów. W aplikacjach klienckich (np. Apollo Client, Relay) zmienne są automatycznie serializowane i deserializowane, co upraszcza pracę z API.
Zmienne są szczególnie istotne w mutations, gdzie często przekazuje się złożone obiekty input. Stosowanie zmiennych zamiast wartości literalnych jest uważane za best practice w GraphQL i jest wymagane przez wiele narzędzi do optymalizacji zapytań (jak persisted queries).
Przykład kodu:
# Podstawowa definicja i użycie zmiennej
query GetUser($userId: ID!) {
user(id: $userId) {
name
email
}
}
# Zmienne przekazane osobno (JSON):
# {
# "userId": "123"
# }
# Wiele zmiennych z różnymi typami
query GetPosts(
$authorId: ID!
$limit: Int!
$status: PostStatus
$tags: [String!]
) {
posts(
authorId: $authorId
limit: $limit
status: $status
tags: $tags
) {
id
title
status
tags
}
}
# Zmienne:
# {
# "authorId": "456",
# "limit": 10,
# "status": "PUBLISHED",
# "tags": ["graphql", "tutorial"]
# }
# Zmienne z wartościami domyślnymi
query SearchProducts(
$query: String!
$category: String = "all"
$minPrice: Float = 0
$maxPrice: Float = 10000
$inStock: Boolean = true
) {
products(
search: $query
category: $category
priceRange: { min: $minPrice, max: $maxPrice }
inStock: $inStock
) {
id
name
price
stockCount
}
}
# Minimalne zmienne (reszta użyje wartości domyślnych):
# {
# "query": "laptop"
# }
# Lub pełne zmienne:
# {
# "query": "laptop",
# "category": "electronics",
# "minPrice": 500,
# "maxPrice": 2000,
# "inStock": false
# }
# Zmienne z typami Input Object
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
username
email
profile {
firstName
lastName
}
}
errors {
field
message
}
}
}
# Zmienne z kompleksowym obiektem:
# {
# "input": {
# "username": "johndoe",
# "email": "john@example.com",
# "password": "securePassword123",
# "profile": {
# "firstName": "John",
# "lastName": "Doe",
# "bio": "GraphQL enthusiast"
# },
# "preferences": {
# "newsletter": true,
# "notifications": {
# "email": true,
# "push": false
# }
# }
# }
# }
# Zmienne z tablicami i zagnieżdżonymi typami
mutation UpdateProduct(
$id: ID!
$updates: ProductUpdateInput!
$addTags: [String!]
$removeTags: [String!]
) {
updateProduct(
id: $id
updates: $updates
addTags: $addTags
removeTags: $removeTags
) {
id
name
tags
updatedAt
}
}
# Zmienne:
# {
# "id": "789",
# "updates": {
# "name": "Updated Product Name",
# "price": 99.99,
# "description": "New description"
# },
# "addTags": ["featured", "sale"],
# "removeTags": ["old-tag"]
# }
# Nullable vs Non-nullable zmienne
query FlexibleSearch(
$required: String! # Wymagana - musi być przekazana
$optional: String # Opcjonalna - może być null lub pominięta
$withDefault: Int = 10 # Ma wartość domyślną
$requiredList: [String!]! # Wymagana lista, elementy nie mogą być null
$optionalList: [String] # Opcjonalna lista, elementy mogą być null
) {
search(
query: $required
category: $optional
limit: $withDefault
tags: $requiredList
excludeTags: $optionalList
) {
results {
id
title
}
}
}
# Używanie zmiennych w dyrektywach
query GetUserProfile($userId: ID!, $includeStats: Boolean!) {
user(id: $userId) {
id
username
email
stats @include(if: $includeStats) {
postCount
followerCount
followingCount
}
}
}
# Zmienne:
# {
# "userId": "123",
# "includeStats": true
# }
# Fragment z zmiennymi
query GetMultipleUsers($userIds: [ID!]!, $includePrivate: Boolean = false) {
users(ids: $userIds) {
...UserFields
privateData @include(if: $includePrivate) {
phoneNumber
address
}
}
}
fragment UserFields on User {
id
username
email
avatar
}
Materiały
Czym są aliasy i jak rozwiązują konflikty nazw pól?
Odpowiedź w 30 sekund:
Aliasy w GraphQL pozwalają nadawać niestandardowe nazwy polom w wynikach zapytania, co umożliwia wielokrotne odpytywanie tego samego pola z różnymi argumentami w jednym zapytaniu. Używając składni alias: fieldName, można uniknąć konfliktów nazw i otrzymać dane w bardziej użytecznej strukturze. Jest to szczególnie przydatne przy porównywaniu danych lub pobieraniu wielu wariantów tego samego zasobu.
Odpowiedź w 2 minuty: Aliasy są mechanizmem w GraphQL, który rozwiązuje problem niemożności wielokrotnego użycia tego samego pola w jednym zapytaniu. Bez aliasów, próba zapytania o to samo pole dwukrotnie (nawet z różnymi argumentami) spowodowałaby konflikt nazw w wynikach JSON. Aliasy pozwalają nadać każdemu wystąpieniu pola unikalną nazwę w odpowiedzi, zachowując możliwość używania różnych argumentów.
Składnia aliasu jest prosta: przed nazwą pola podajemy własną nazwę i dwukropek (myAlias: fieldName). W odpowiedzi pole pojawi się pod nazwą aliasu zamiast oryginalnej nazwy. To umożliwia np. pobranie danych tego samego użytkownika z różnymi parametrami, porównanie statystyk z różnych okresów, czy też uzyskanie wielu wersji tego samego zasobu w jednym round-trip do serwera.
Aliasy są szczególnie przydatne w scenariuszach takich jak: porównywanie danych (np. statystyki tego miesiąca vs poprzedniego), pobieranie tego samego typu obiektu dla różnych identyfikatorów, wykonywanie operacji A/B testing, czy też organizowanie danych w bardziej logiczną strukturę niż ta oferowana przez API. Zwiększają one elastyczność zapytań bez konieczności modyfikowania schematu.
Aliasy działają na dowolnym poziomie zagnieżdżenia zapytania i mogą być łączone ze wszystkimi innymi funkcjami GraphQL - zmiennymi, fragmentami, dyrektywami. Nie wpływają na wydajność po stronie serwera - to tylko zmiana nazwy w odpowiedzi. W połączeniu ze zmiennymi, aliasy pozwalają na tworzenie bardzo elastycznych i wielokrotnego użytku zapytań.
Przykład kodu:
# Podstawowe użycie aliasów - pobieranie dwóch różnych użytkowników
query {
firstUser: user(id: "123") {
name
email
}
secondUser: user(id: "456") {
name
email
}
}
# Odpowiedź:
# {
# "data": {
# "firstUser": { "name": "John", "email": "john@example.com" },
# "secondUser": { "name": "Jane", "email": "jane@example.com" }
# }
# }
# Porównywanie danych z różnych okresów
query CompareStats {
thisMonth: stats(period: "2024-03") {
revenue
orderCount
averageOrderValue
}
lastMonth: stats(period: "2024-02") {
revenue
orderCount
averageOrderValue
}
yearAgo: stats(period: "2023-03") {
revenue
orderCount
averageOrderValue
}
}
# To samo pole z różnymi filtrami
query GetFilteredPosts {
publishedPosts: posts(status: PUBLISHED, limit: 10) {
id
title
publishedAt
}
draftPosts: posts(status: DRAFT, limit: 5) {
id
title
updatedAt
}
scheduledPosts: posts(status: SCHEDULED, limit: 5) {
id
title
scheduledFor
}
}
# Aliasy z różnymi sortowaniami
query GetTopContent {
mostRecent: articles(orderBy: CREATED_AT, limit: 5) {
title
createdAt
}
mostPopular: articles(orderBy: VIEWS, limit: 5) {
title
viewCount
}
trending: articles(orderBy: ENGAGEMENT, limit: 5) {
title
engagementScore
}
}
# Pobieranie danych dla wielu kategorii produktów
query GetProductsByCategory {
electronics: products(category: "electronics", limit: 10) {
id
name
price
}
books: products(category: "books", limit: 10) {
id
name
price
}
clothing: products(category: "clothing", limit: 10) {
id
name
price
}
}
# Aliasy na zagnieżdżonych polach
query {
user(id: "123") {
name
recentPosts: posts(limit: 5, orderBy: CREATED_AT) {
title
}
popularPosts: posts(limit: 5, orderBy: LIKES) {
title
likesCount
}
}
}
# Używanie aliasów ze zmiennymi
query CompareUsers(
$firstUserId: ID!
$secondUserId: ID!
$postLimit: Int = 5
) {
user1: user(id: $firstUserId) {
username
email
latestPosts: posts(limit: $postLimit, orderBy: CREATED_AT) {
title
createdAt
}
}
user2: user(id: $secondUserId) {
username
email
latestPosts: posts(limit: $postLimit, orderBy: CREATED_AT) {
title
createdAt
}
}
}
# Zmienne:
# {
# "firstUserId": "123",
# "secondUserId": "456",
# "postLimit": 10
# }
# Aliasy dla lepszej organizacji danych
query GetDashboardData {
currentUser: me {
id
username
unreadCount: notifications(unread: true) {
totalCount
}
allNotifications: notifications(limit: 20) {
id
message
createdAt
}
}
}
# A/B testing - różne wersje tego samego contentu
query ABTestContent($experimentId: ID!) {
variantA: content(experimentId: $experimentId, variant: "A") {
headline
bodyText
imageUrl
}
variantB: content(experimentId: $experimentId, variant: "B") {
headline
bodyText
imageUrl
}
}
# Kompleksny przykład z wieloma aliasami
query AnalyticsDashboard($userId: ID!) {
user(id: $userId) {
username
# Statystyki z różnych okresów
today: analytics(period: TODAY) {
pageViews
uniqueVisitors
bounceRate
}
thisWeek: analytics(period: THIS_WEEK) {
pageViews
uniqueVisitors
bounceRate
}
thisMonth: analytics(period: THIS_MONTH) {
pageViews
uniqueVisitors
bounceRate
}
# Posty z różnymi filtrami
topPerforming: posts(orderBy: ENGAGEMENT, limit: 5) {
title
views
likes
comments
}
recentlyPublished: posts(orderBy: PUBLISHED_AT, limit: 5) {
title
publishedAt
}
needsAttention: posts(filter: { hasReportedContent: true }) {
title
reportCount
}
}
}
# Aliasy z fragmentami
query GetRelatedUsers($userId: ID!) {
mainUser: user(id: $userId) {
...UserInfo
}
topFollower: user(id: $userId) {
followers(limit: 1, orderBy: INFLUENCE) {
...UserInfo
}
}
suggestedConnection: user(id: $userId) {
suggestedFriends(limit: 1) {
...UserInfo
}
}
}
fragment UserInfo on User {
id
username
avatar
followerCount
}
Materiały
Jak projektować mutations dla operacji CRUD?
Odpowiedź w 30 sekund: Mutations w GraphQL dla operacji CRUD (Create, Read, Update, Delete) powinny być projektowane z myślą o jasnej semantyce, type safety i obsłudze błędów. Wzorce obejmują używanie Input Types dla danych wejściowych, zwracanie payload objects zawierających zarówno utworzony/zmodyfikowany obiekt jak i ewentualne błędy, oraz stosowanie konwencji nazewnictwa (create*, update*, delete*). Kluczowe jest również optymistyczne UI updates i cache invalidation.
Odpowiedź w 2 minuty:
Projektowanie mutations dla operacji CRUD wymaga przemyślanego podejścia, które łączy prostotę użycia z elastycznością i bezpieczeństwem. Podstawową zasadą jest używanie Input Types dla wszystkich danych wejściowych - grupują one pola w logiczne całości, ułatwiają walidację i umożliwiają ewolucję API bez breaking changes. Konwencja nazewnictwa powinna być spójna: createX, updateX, deleteX dla podstawowych operacji.
Mutations powinny zwracać payload objects zamiast bezpośrednio obiektów domeny. Payload zawiera typowo pole z utworzonym/zmodyfikowanym obiektem, listę błędów walidacji oraz opcjonalnie dodatkowe metadane. Ten wzorzec umożliwia zwracanie częściowych sukcesów i szczegółowych informacji o błędach. Przykład: { user: User, errors: [Error], clientMutationId: String }.
W operacjach UPDATE należy rozróżnić między pełnym zastąpieniem (PUT-like) a częściową aktualizacją (PATCH-like). Często stosuje się wzorzec z częściowym input type, gdzie wszystkie pola są opcjonalne, a null oznacza "nie zmieniaj tego pola". DELETE mutations powinny zwracać informację o sukcesie oraz opcjonalnie usunięty obiekt (dla optimistic UI) lub ID usuniętego rekordu.
Kluczowe aspekty to również: autoryzacja (sprawdzanie uprawnień w resolverach), optymistyczne aktualizacje UI (zwracanie pełnych danych dla cache update), atomowość operacji (transakcje dla złożonych mutations), oraz idempotentność (używanie clientMutationId). Dla złożonych operacji warto rozważyć bulk mutations lub nested mutations, które wykonują wiele powiązanych zmian w jednej transakcji.
Przykład kodu:
# ===== SCHEMAT - Definicje typów =====
# CREATE - Tworzenie nowego użytkownika
input CreateUserInput {
username: String!
email: String!
password: String!
profile: UserProfileInput
clientMutationId: String # Dla idempotentności
}
input UserProfileInput {
firstName: String
lastName: String
bio: String
avatar: Upload
}
type CreateUserPayload {
user: User
errors: [UserError!]
clientMutationId: String
}
type UserError {
field: String
message: String!
code: ErrorCode!
}
enum ErrorCode {
VALIDATION_ERROR
DUPLICATE_EMAIL
WEAK_PASSWORD
UNAUTHORIZED
}
# UPDATE - Aktualizacja użytkownika (częściowa)
input UpdateUserInput {
id: ID!
username: String # Wszystkie pola opcjonalne
email: String
profile: UserProfileInput
clientMutationId: String
}
type UpdateUserPayload {
user: User
errors: [UserError!]
clientMutationId: String
}
# DELETE - Usuwanie użytkownika
input DeleteUserInput {
id: ID!
clientMutationId: String
}
type DeleteUserPayload {
deletedUserId: ID # ID usuniętego obiektu
user: User # Opcjonalnie: dane usuniętego obiektu dla UI
success: Boolean!
errors: [UserError!]
clientMutationId: String
}
# ===== MUTATIONS - Użycie =====
# CREATE - Tworzenie nowego użytkownika
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
user {
id
username
email
profile {
firstName
lastName
}
createdAt
}
errors {
field
message
code
}
clientMutationId
}
}
# Zmienne:
# {
# "input": {
# "username": "johndoe",
# "email": "john@example.com",
# "password": "SecurePass123!",
# "profile": {
# "firstName": "John",
# "lastName": "Doe",
# "bio": "GraphQL developer"
# },
# "clientMutationId": "create-user-1"
# }
# }
# UPDATE - Częściowa aktualizacja
mutation UpdateUser($input: UpdateUserInput!) {
updateUser(input: $input) {
user {
id
username
email
profile {
firstName
lastName
bio
}
updatedAt
}
errors {
field
message
code
}
}
}
# Zmienne (tylko pola do aktualizacji):
# {
# "input": {
# "id": "123",
# "profile": {
# "bio": "Senior GraphQL developer"
# }
# }
# }
# DELETE - Usuwanie użytkownika
mutation DeleteUser($input: DeleteUserInput!) {
deleteUser(input: $input) {
deletedUserId
success
errors {
message
code
}
}
}
# Zmienne:
# {
# "input": {
# "id": "123"
# }
# }
# ===== ZAAWANSOWANE WZORCE =====
# BULK CREATE - Tworzenie wielu obiektów naraz
input BulkCreatePostsInput {
posts: [CreatePostInput!]!
}
type BulkCreatePostsPayload {
posts: [Post!]
errors: [BulkOperationError!]
successCount: Int!
failureCount: Int!
}
type BulkOperationError {
index: Int! # Indeks w tablicy
field: String
message: String!
}
mutation BulkCreatePosts($input: BulkCreatePostsInput!) {
bulkCreatePosts(input: $input) {
posts {
id
title
}
successCount
failureCount
errors {
index
message
}
}
}
# UPSERT - Stwórz lub zaktualizuj
input UpsertProductInput {
sku: String! # Unikalny identyfikator
name: String!
price: Float!
description: String
}
type UpsertProductPayload {
product: Product!
wasCreated: Boolean! # true = created, false = updated
errors: [ProductError!]
}
mutation UpsertProduct($input: UpsertProductInput!) {
upsertProduct(input: $input) {
product {
id
sku
name
price
}
wasCreated
}
}
# NESTED MUTATION - Złożona operacja z relacjami
input CreatePostWithTagsInput {
title: String!
content: String!
authorId: ID!
tags: [CreateTagInput!] # Tworzenie powiązanych tagów
categoryId: ID
}
input CreateTagInput {
name: String!
color: String
}
type CreatePostPayload {
post: Post
createdTags: [Tag!] # Nowo utworzone tagi
errors: [PostError!]
}
mutation CreatePostWithTags($input: CreatePostWithTagsInput!) {
createPost(input: $input) {
post {
id
title
tags {
id
name
color
}
author {
username
}
}
createdTags {
id
name
}
errors {
field
message
}
}
}
# PARTIAL UPDATE z NULL handling
input UpdatePostInput {
id: ID!
title: String
content: String
publishedAt: DateTime # null = ustaw na null
clearPublishedAt: Boolean # explicit flag do czyszczenia
addTags: [ID!]
removeTags: [ID!]
}
mutation UpdatePost($input: UpdatePostInput!) {
updatePost(input: $input) {
post {
id
title
content
publishedAt
tags {
id
name
}
updatedAt
}
errors {
field
message
}
}
}
# SOFT DELETE z opcją przywrócenia
input DeletePostInput {
id: ID!
permanent: Boolean = false # false = soft delete, true = hard delete
}
type DeletePostPayload {
deletedPostId: ID
post: Post # Dla soft delete, null dla permanent
success: Boolean!
}
mutation DeletePost($input: DeletePostInput!) {
deletePost(input: $input) {
deletedPostId
post {
id
deletedAt
isDeleted
}
success
}
}
# RESTORE - Przywracanie soft-deleted obiektów
mutation RestorePost($id: ID!) {
restorePost(id: $id) {
post {
id
title
deletedAt
isDeleted
}
success
}
}
# TRANSACTION - Wiele operacji w jednej transakcji
input TransferOwnershipInput {
postId: ID!
fromUserId: ID!
toUserId: ID!
transferComments: Boolean = true
}
type TransferOwnershipPayload {
post: Post
previousOwner: User
newOwner: User
transferredCommentsCount: Int
errors: [TransferError!]
}
mutation TransferOwnership($input: TransferOwnershipInput!) {
transferPostOwnership(input: $input) {
post {
id
title
author {
id
username
}
}
previousOwner {
username
}
newOwner {
username
}
transferredCommentsCount
}
}
Materiały
Czym są dyrektywy @skip i @include i kiedy ich używać?
Odpowiedź w 30 sekund: Dyrektywy @skip i @include to wbudowane mechanizmy GraphQL pozwalające na warunkowe włączanie lub pomijanie pól w zapytaniu na podstawie zmiennych. @include(if: $condition) dodaje pole gdy warunek jest true, a @skip(if: $condition) pomija pole gdy warunek jest true. Są przydatne do tworzenia elastycznych, wielokrotnego użytku zapytań, które dostosowują się do różnych kontekstów bez konieczności definiowania wielu wariantów zapytania.
Odpowiedź w 2 minuty: Dyrektywy @skip i @include są częścią specyfikacji GraphQL i umożliwiają dynamiczne modyfikowanie struktury zapytania w runtime na podstawie wartości zmiennych boolean. Zamiast tworzyć wiele różnych zapytań dla różnych scenariuszy, można użyć jednego zapytania z dyrektywami, które warunkowo załączają lub pomijają określone pola. To zwiększa reużywalność zapytań i redukuje ilość kodu.
Dyrektywa @include(if: $variable) dodaje pole do zapytania tylko gdy zmienna ma wartość true. Jest użyteczna gdy chcemy opcjonalnie pobrać dodatkowe dane, np. szczegółowe informacje tylko dla zalogowanych użytkowników czy statystyki tylko w panelu administracyjnym. Przeciwnie, @skip(if: $variable) pomija pole gdy zmienna jest true - przydatne gdy chcemy wykluczyć pewne dane w określonych warunkach.
Te dyrektywy mogą być stosowane na polach, fragmentach i inline fragments, co daje dużą elastyczność. Szczególnie przydatne są w aplikacjach mobilnych (oszczędność transferu danych), interfejsach z różnymi poziomami uprawnień (pokazywanie/ukrywanie wrażliwych danych), oraz w sytuacjach gdzie różne komponenty UI potrzebują różnych podzbiorów danych z tego samego źródła.
Należy jednak pamiętać o cache implications - zapytania z różnymi wartościami dyrektyw są traktowane jako różne zapytania przez cache mechanizmy. W praktyce często lepiej jest używać fragmentów lub osobnych zapytań dla radykalnie różnych struktur danych, a dyrektywy rezerwować dla mniejszych, opcjonalnych różnic w wymaganych danych.
Przykład kodu:
# Podstawowe użycie @include
query GetUser($userId: ID!, $includeProfile: Boolean!) {
user(id: $userId) {
id
username
email
# Profile jest pobierany tylko gdy $includeProfile = true
profile @include(if: $includeProfile) {
firstName
lastName
bio
avatar
}
}
}
# Zmienne dla włączonego profilu:
# { "userId": "123", "includeProfile": true }
# Zmienne dla wyłączonego profilu:
# { "userId": "123", "includeProfile": false }
# Podstawowe użycie @skip
query GetPost($postId: ID!, $skipComments: Boolean!) {
post(id: $postId) {
id
title
content
# Komentarze są pomijane gdy $skipComments = true
comments @skip(if: $skipComments) {
id
content
author {
username
}
}
}
}
# Wiele dyrektyw w jednym zapytaniu
query GetDashboard(
$userId: ID!
$includeStats: Boolean!
$includePrivateData: Boolean!
$skipOldPosts: Boolean!
) {
user(id: $userId) {
username
email
# Statystyki tylko dla admina
stats @include(if: $includeStats) {
postCount
followerCount
totalViews
}
# Prywatne dane tylko dla właściciela
privateData @include(if: $includePrivateData) {
phoneNumber
address
birthDate
}
# Pomijamy stare posty w mobilnej wersji
posts(limit: 20) @skip(if: $skipOldPosts) {
id
title
createdAt
}
}
}
# Dyrektywy na fragmentach
query GetProduct($productId: ID!, $includeReviews: Boolean!) {
product(id: $productId) {
id
name
price
...ProductReviews @include(if: $includeReviews)
}
}
fragment ProductReviews on Product {
reviews(limit: 10) {
id
rating
comment
author {
username
}
}
averageRating
reviewCount
}
# Dyrektywy na inline fragments (z interface/union)
query GetSearchResults($query: String!, $includeUserDetails: Boolean!) {
search(query: $query) {
... on User {
id
username
# Szczegóły tylko gdy potrzebne
profile @include(if: $includeUserDetails) {
bio
location
website
}
}
... on Post {
id
title
excerpt
}
}
}
# Kombinacja @skip i @include
query GetUserContent(
$userId: ID!
$includeDrafts: Boolean!
$skipArchived: Boolean!
) {
user(id: $userId) {
username
# Pobierz drafty tylko gdy includeDrafts = true
drafts @include(if: $includeDrafts) {
id
title
updatedAt
}
# Pomiń zarchiwizowane gdy skipArchived = true
archivedPosts @skip(if: $skipArchived) {
id
title
archivedAt
}
}
}
# Praktyczny przykład - różne poziomy szczegółowości
query GetArticle(
$articleId: ID!
$isDetailedView: Boolean!
$isPreview: Boolean!
) {
article(id: $articleId) {
id
title
# Krótki excerpt w preview
excerpt @include(if: $isPreview)
# Pełna treść w widoku szczegółowym
content @include(if: $isDetailedView)
# Autor tylko w widoku szczegółowym
author @include(if: $isDetailedView) {
id
username
avatar
bio @include(if: $isDetailedView)
}
# Powiązane artykuły tylko w widoku szczegółowym
relatedArticles(limit: 5) @include(if: $isDetailedView) {
id
title
excerpt
}
# Statystyki pomijane w preview
stats @skip(if: $isPreview) {
viewCount
likeCount
shareCount
}
}
}
# Użycie z uprawnieniami użytkownika
query GetUserProfile(
$userId: ID!
$isOwner: Boolean!
$isAdmin: Boolean!
) {
user(id: $userId) {
id
username
avatar
# Email widoczny tylko dla właściciela lub admina
email @include(if: $isOwner)
# Panel admina
adminPanel @include(if: $isAdmin) {
accountStatus
lastLogin
ipAddress
flags
}
# Prywatne ustawienia tylko dla właściciela
settings @include(if: $isOwner) {
notifications {
email
push
sms
}
privacy {
profileVisibility
showEmail
}
}
}
}
# Optymalizacja dla mobile vs desktop
query GetFeed(
$limit: Int!
$isMobile: Boolean!
$includeMedia: Boolean!
) {
feed(limit: $limit) {
id
text
createdAt
author {
username
avatar
# Bio tylko na desktop
bio @skip(if: $isMobile)
followerCount @skip(if: $isMobile)
}
# Media opcjonalnie (dla oszczędności danych)
media @include(if: $includeMedia) {
type
url
thumbnail
}
# Reakcje zawsze, ale szczegóły tylko na desktop
reactions {
count
userReacted
breakdown @skip(if: $isMobile) {
like
love
laugh
}
}
# Komentarze pomijane na mobile dla szybkości
comments @skip(if: $isMobile) {
id
text
author {
username
}
}
}
}
# Warunkowe pobieranie relacji
query GetProject(
$projectId: ID!
$includeTeam: Boolean!
$includeTasks: Boolean!
$includeFiles: Boolean!
) {
project(id: $projectId) {
id
name
description
team @include(if: $includeTeam) {
id
members {
id
name
role
}
}
tasks @include(if: $includeTasks) {
id
title
status
assignee {
name
}
}
files @include(if: $includeFiles) {
id
name
size
uploadedAt
}
}
}
# Zmienne w zależności od kontekstu:
# Szybki widok (lista projektów):
# { "projectId": "123", "includeTeam": false, "includeTasks": false, "includeFiles": false }
# Pełny widok (szczegóły projektu):
# { "projectId": "123", "includeTeam": true, "includeTasks": true, "includeFiles": true }
Materiały
Resolvers
18. Czym są resolvery w GraphQL i jak działają?
Odpowiedź w 30 sekund: Resolvery to funkcje, które określają jak pobrać dane dla każdego pola w schemacie GraphQL. Każde pole w schemacie może mieć własny resolver, który wykonuje się gdy pole jest żądane w zapytaniu. Resolvery są odpowiedzialne za pobieranie danych z baz danych, API zewnętrznych lub innych źródeł.
Odpowiedź w 2 minuty: Resolvery są fundamentalnym mechanizmem w GraphQL, który łączy schemat z faktycznymi danymi. Każdy resolver to funkcja, która wie jak pobrać lub obliczyć wartość dla konkretnego pola w schemacie. Gdy serwer GraphQL otrzymuje zapytanie, przechodzi przez schemat i wykonuje odpowiednie resolvery dla każdego żądanego pola.
Resolvery działają hierarchicznie - zaczynając od pól głównych (root fields) i schodząc w dół przez zagnieżdżone obiekty. Jeśli pole nie ma zdefiniowanego resolvera, GraphQL używa domyślnego resolvera, który po prostu zwraca właściwość o tej samej nazwie z obiektu nadrzędnego. To pozwala na pisanie resolverów tylko tam, gdzie potrzebna jest specjalna logika.
Kluczową zaletą resolverów jest ich elastyczność - mogą pobierać dane z dowolnego źródła (bazy danych, REST API, mikroserwisy, cache), wykonywać walidację, transformować dane, lub nawet agregować informacje z wielu źródeł. Resolvery mogą być asynchroniczne, co pozwala na efektywne zarządzanie operacjami I/O.
GraphQL automatycznie czeka na rozwiązanie wszystkich resolverów przed zwróceniem odpowiedzi, co znacznie upraszcza zarządzanie asynchronicznymi operacjami w porównaniu z tradycyjnymi REST API.
Przykład kodu:
// Definicja schematu GraphQL
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
type Query {
user(id: ID!): User
allUsers: [User!]!
}
`;
// Resolvery dla każdego pola
const resolvers = {
// Resolvery dla głównych zapytań (root resolvers)
Query: {
// Resolver dla pojedynczego użytkownika
user: async (parent, args, context) => {
// Pobieranie z bazy danych
return await context.db.users.findById(args.id);
},
// Resolver dla wszystkich użytkowników
allUsers: async (parent, args, context) => {
return await context.db.users.findAll();
}
},
// Resolvery dla pól typu User
User: {
// Resolver dla pola 'posts' - pobiera posty użytkownika
posts: async (parent, args, context) => {
// 'parent' to obiekt User zwrócony przez resolver nadrzędny
return await context.db.posts.findByAuthorId(parent.id);
}
// Pola id, name, email nie potrzebują resolverów
// GraphQL użyje domyślnego resolvera, który zwróci parent.id, parent.name, etc.
},
// Resolvery dla pól typu Post
Post: {
// Resolver dla pola 'author' - pobiera autora posta
author: async (parent, args, context) => {
// Wykorzystanie DataLoader do optymalizacji zapytań
return await context.loaders.userLoader.load(parent.authorId);
}
}
};
// Przykład użycia z Apollo Server
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
// Kontekst dostępny we wszystkich resolverach
db: database,
loaders: createLoaders(),
user: req.user // Zalogowany użytkownik
})
});
// Przykładowe zapytanie i przepływ wykonania:
/*
query {
user(id: "123") { # 1. Wykonuje Query.user resolver
name # 2. Domyślny resolver zwraca parent.name
email # 3. Domyślny resolver zwraca parent.email
posts { # 4. Wykonuje User.posts resolver
title # 5. Domyślny resolver zwraca parent.title
author { # 6. Wykonuje Post.author resolver
name # 7. Domyślny resolver zwraca parent.name
}
}
}
}
*/
Materiały:
19. Jak działa łańcuch resolverów (resolver chain) dla zagnieżdżonych pól?
Odpowiedź w 30 sekund:
Łańcuch resolverów działa hierarchicznie - zaczynając od resolverów głównych (root), a następnie wykonując resolvery dla każdego zagnieżdżonego pola. Wynik każdego resolvera staje się argumentem parent dla resolverów jego pól potomnych. GraphQL automatycznie zarządza kolejnością wykonywania, czekając na asynchroniczne operacje.
Odpowiedź w 2 minuty: Łańcuch resolverów to mechanizm, który pozwala GraphQL na efektywne rozwiązywanie złożonych, zagnieżdżonych zapytań. Proces rozpoczyna się od resolverów głównych (root resolvers) zdefiniowanych w typach Query, Mutation lub Subscription. Każdy z tych resolverów zwraca obiekt lub listę obiektów, które następnie stają się danymi wejściowymi dla resolverów pól potomnych.
Kluczowym aspektem łańcucha resolverów jest to, że wynik każdego resolvera jest przekazywany jako pierwszy argument (parent) do resolverów jego pól zagnieżdżonych. To pozwala na naturalne przepływanie danych przez strukturę zapytania. GraphQL wykonuje resolvery w odpowiedniej kolejności, automatycznie zarządzając zależnościami między nimi.
Gdy resolver zwraca Promise (asynchroniczny), GraphQL czeka na jego rozwiązanie przed wykonaniem resolverów potomnych. To pozwala na pisanie czystego, asynchronicznego kodu bez konieczności ręcznego zarządzania kolejnością operacji. GraphQL również optymalizuje wykonywanie - resolvery na tym samym poziomie mogą być wykonywane równolegle, co znacznie poprawia wydajność.
Ważne jest zrozumienie, że każde pole może mieć własny resolver, ale nie musi. Jeśli resolver nie jest zdefiniowany, GraphQL użyje domyślnego resolvera, który po prostu zwraca właściwość o tej samej nazwie z obiektu parent. To pozwala na definiowanie resolverów tylko tam, gdzie potrzebna jest specjalna logika, co znacznie upraszcza kod.
Przykład kodu:
// Schemat GraphQL z zagnieżdżonymi polami
const typeDefs = `
type Query {
organization(id: ID!): Organization
}
type Organization {
id: ID!
name: String!
departments: [Department!]!
}
type Department {
id: ID!
name: String!
employees: [Employee!]!
}
type Employee {
id: ID!
firstName: String!
lastName: String!
fullName: String!
manager: Employee
}
`;
// Resolvery pokazujące przepływ danych przez łańcuch
const resolvers = {
Query: {
// KROK 1: Root resolver - punkt startowy łańcucha
organization: async (parent, args, context) => {
console.log('1. Query.organization - parent:', parent); // null dla root
const org = await context.db.organizations.findById(args.id);
console.log(' Zwracam organization:', org);
return org; // Ten obiekt będzie 'parent' dla Organization resolverów
}
},
Organization: {
// KROK 2: Resolver dla zagnieżdżonego pola 'departments'
departments: async (parent, args, context) => {
console.log('2. Organization.departments - parent:', parent);
// 'parent' to obiekt Organization z poprzedniego resolvera
const depts = await context.db.departments.findByOrgId(parent.id);
console.log(' Zwracam departments:', depts);
return depts; // Każdy obiekt będzie 'parent' dla Department resolverów
}
// Pola id i name nie potrzebują resolverów
// Domyślny resolver zwróci parent.id i parent.name
},
Department: {
// KROK 3: Resolver dla kolejnego zagnieżdżonego pola 'employees'
employees: async (parent, args, context) => {
console.log('3. Department.employees - parent:', parent);
// 'parent' to pojedynczy obiekt Department z poprzedniego resolvera
const emps = await context.db.employees.findByDeptId(parent.id);
console.log(' Zwracam employees:', emps);
return emps; // Każdy obiekt będzie 'parent' dla Employee resolverów
}
},
Employee: {
// KROK 4: Pole wyliczane - nie ma w bazie danych
fullName: (parent) => {
console.log('4. Employee.fullName - parent:', parent);
// 'parent' to pojedynczy obiekt Employee
const result = `${parent.firstName} ${parent.lastName}`;
console.log(' Zwracam fullName:', result);
return result; // Synchroniczny resolver
},
// KROK 5: Kolejne zagnieżdżenie - manager też jest Employee
manager: async (parent, args, context) => {
console.log('5. Employee.manager - parent:', parent);
if (!parent.managerId) return null;
const manager = await context.db.employees.findById(parent.managerId);
console.log(' Zwracam manager:', manager);
return manager; // Ten obiekt może uruchomić kolejny łańcuch Employee resolverów
}
}
};
// Przykładowe zapytanie i kolejność wykonywania resolverów
/*
query {
organization(id: "org-1") { # RESOLVER 1: Query.organization
name # Domyślny resolver
departments { # RESOLVER 2: Organization.departments (dla org-1)
name # Domyślny resolver
employees { # RESOLVER 3a: Department.employees (dla dept-1)
# RESOLVER 3b: Department.employees (dla dept-2)
# Wykonywane równolegle!
fullName # RESOLVER 4: Employee.fullName (dla każdego emp)
manager { # RESOLVER 5: Employee.manager (dla każdego emp)
fullName # RESOLVER 4: Employee.fullName (dla każdego managera)
}
}
}
}
}
Przepływ danych przez łańcuch:
┌─────────────────────────────────────────────────────────────┐
│ Query.organization │
│ parent: null │
│ return: { id: "org-1", name: "Acme Corp" } │
└─────────────────────────┬───────────────────────────────────┘
│ parent dla następnego poziomu
▼
┌─────────────────────────────────────────────────────────────┐
│ Organization.departments │
│ parent: { id: "org-1", name: "Acme Corp" } │
│ return: [{ id: "dept-1" }, { id: "dept-2" }] │
└──────────┬────────────────────────────┬─────────────────────┘
│ │
│ parent dla dept-1 │ parent dla dept-2
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ Department.employees │ │ Department.employees │
│ parent: { id: "dept-1" } │ │ parent: { id: "dept-2" } │
│ return: [employees...] │ │ return: [employees...] │
└──────────┬───────────────┘ └──────────┬───────────────┘
│ │
└──────────┬───────────────────┘
│ parent dla każdego employee
▼
┌──────────────────────────┐
│ Employee.fullName │
│ Employee.manager │
└──────────────────────────┘
*/
// Przykład z logowaniem kolejności wykonywania
async function demonstrateResolverChain() {
const result = await graphqlServer.executeOperation({
query: `
query {
organization(id: "org-1") {
name
departments {
name
employees {
fullName
}
}
}
}
`
});
// Console output pokazuje kolejność wykonywania:
// 1. Query.organization - parent: null
// Zwracam organization: { id: "org-1", name: "Acme Corp" }
// 2. Organization.departments - parent: { id: "org-1", name: "Acme Corp" }
// Zwracam departments: [{ id: "dept-1" }, { id: "dept-2" }]
// 3. Department.employees - parent: { id: "dept-1" }
// Zwracam employees: [{ id: "emp-1" }, { id: "emp-2" }]
// 3. Department.employees - parent: { id: "dept-2" } (równolegle!)
// Zwracam employees: [{ id: "emp-3" }]
// 4. Employee.fullName - parent: { id: "emp-1", firstName: "Jan" ... }
// Zwracam fullName: "Jan Kowalski"
// ... itd.
}
Materiały:
20. Jakie argumenty otrzymuje funkcja resolvera (parent, args, context, info)?
Odpowiedź w 30 sekund:
Każdy resolver w GraphQL otrzymuje cztery argumenty: parent (wynik resolvera nadrzędnego), args (argumenty zapytania), context (współdzielone dane jak połączenie z bazą, użytkownik), oraz info (metadane o zapytaniu). Te argumenty pozwalają resolverowi pobrać dane, wykonać autoryzację i zoptymalizować zapytania.
Odpowiedź w 2 minuty: Funkcja resolvera w GraphQL ma zawsze tę samą sygnaturę z czterema argumentami, które dostarczają wszystkich informacji potrzebnych do rozwiązania zapytania.
Pierwszy argument parent (lub root) zawiera wynik zwrócony przez resolver nadrzędny w łańcuchu. Dla resolverów głównych (Query, Mutation, Subscription) wartość ta jest zwykle undefined lub null. Dla zagnieżdżonych pól, parent zawiera obiekt zwrócony przez resolver wyższego poziomu, co pozwala na dostęp do danych kontekstowych przy rozwiązywaniu zagnieżdżonych pól.
Drugi argument args to obiekt zawierający wszystkie argumenty przekazane do pola w zapytaniu GraphQL. Jeśli zapytanie to user(id: "123"), to args będzie { id: "123" }. To główny sposób przekazywania parametrów do resolverów.
Trzeci argument context to obiekt współdzielony między wszystkimi resolverami w danym zapytaniu. Jest tworzony raz na zapytanie i zazwyczaj zawiera rzeczy jak połączenia z bazą danych, informacje o zalogowanym użytkowniku, DataLoadery, czy tokeny autoryzacyjne. Context jest idealnym miejscem do umieszczania zasobów, które powinny być dostępne w całym zapytaniu.
Czwarty argument info zawiera informacje o stanie wykonywania zapytania - drzewo AST zapytania, nazwę pola, ścieżkę do obecnego pola, oraz wiele innych metadanych. Jest szczególnie przydatny do zaawansowanych optymalizacji, jak warunkowe ładowanie pól, czy generowanie specyficznych zapytań SQL na podstawie żądanych pól.
Przykład kodu:
// Definicja typów GraphQL
const typeDefs = `
type Query {
user(id: ID!, includeInactive: Boolean = false): User
users(limit: Int, offset: Int): [User!]!
}
type User {
id: ID!
name: String!
email: String!
posts(limit: Int): [Post!]!
isActive: Boolean!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
`;
const resolvers = {
Query: {
// Przykład użycia wszystkich czterech argumentów
user: async (parent, args, context, info) => {
// 1. PARENT - null dla root resolverów
console.log('Parent:', parent); // null lub undefined
// 2. ARGS - argumenty z zapytania
console.log('Args:', args);
// { id: "123", includeInactive: false }
const { id, includeInactive } = args;
// 3. CONTEXT - współdzielone zasoby
console.log('Context keys:', Object.keys(context));
// ['db', 'currentUser', 'loaders', 'req']
// Sprawdzenie autoryzacji używając context
if (!context.currentUser) {
throw new Error('Musisz być zalogowany');
}
// 4. INFO - metadane o zapytaniu
console.log('Field name:', info.fieldName); // 'user'
console.log('Return type:', info.returnType); // 'User'
console.log('Parent type:', info.parentType); // 'Query'
// Sprawdzenie jakie pola są żądane w zapytaniu
const requestedFields = getRequestedFields(info);
console.log('Requested fields:', requestedFields);
// ['id', 'name', 'email', 'posts']
// Optymalizacja - pobierz tylko potrzebne dane
const user = await context.db.users.findById(id, {
includeInactive,
fields: requestedFields
});
return user;
},
// Przykład z wieloma argumentami
users: async (parent, args, context, info) => {
// Destructuring argumentów
const { limit = 10, offset = 0 } = args;
// Walidacja argumentów
if (limit > 100) {
throw new Error('Limit nie może przekraczać 100');
}
// Użycie context do logowania
context.logger.info('Fetching users', { limit, offset });
return await context.db.users.findAll({ limit, offset });
}
},
User: {
// Parent zawiera dane użytkownika z poprzedniego resolvera
posts: async (parent, args, context, info) => {
// 1. PARENT - obiekt User z Query.user
console.log('Parent user:', parent);
// { id: "123", name: "Jan Kowalski", email: "jan@example.com" }
// Dostęp do danych z parent
const userId = parent.id;
// 2. ARGS - limit dla postów
const { limit = 10 } = args;
// 3. CONTEXT - użycie DataLoader do optymalizacji
// DataLoader zbiera wszystkie zapytania i wykonuje batch request
if (context.loaders && context.loaders.postsByUserId) {
return await context.loaders.postsByUserId.load(userId);
}
// Fallback bez DataLoader
return await context.db.posts.findByUserId(userId, { limit });
},
// Przykład pola obliczanego
isActive: (parent, args, context, info) => {
// Pole może być obliczone na podstawie parent
return parent.lastLoginDate &&
(Date.now() - parent.lastLoginDate) < 30 * 24 * 60 * 60 * 1000;
}
},
Post: {
author: async (parent, args, context, info) => {
// PARENT zawiera dane posta
console.log('Parent post:', parent);
// { id: "456", title: "...", authorId: "123" }
// Użycie DataLoader z context dla N+1 problem
return await context.loaders.userById.load(parent.authorId);
}
}
};
// Funkcja pomocnicza do wyciągania żądanych pól z info
function getRequestedFields(info) {
const selections = info.fieldNodes[0].selectionSet?.selections || [];
return selections
.filter(sel => sel.kind === 'Field')
.map(sel => sel.name.value);
}
// Funkcja pomocnicza do głębokiej analizy żądanych pól
function getDeepRequestedFields(info, maxDepth = 5) {
function extractFields(selections, depth = 0) {
if (depth > maxDepth) return [];
return selections.reduce((fields, selection) => {
if (selection.kind === 'Field') {
const field = {
name: selection.name.value,
children: selection.selectionSet
? extractFields(selection.selectionSet.selections, depth + 1)
: []
};
fields.push(field);
}
return fields;
}, []);
}
return extractFields(info.fieldNodes[0].selectionSet?.selections || []);
}
// Tworzenie context dla każdego zapytania
const { ApolloServer } = require('apollo-server');
const DataLoader = require('dataloader');
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
// Context jest tworzony raz na zapytanie
// Autoryzacja - wyciągnięcie użytkownika z tokena
const token = req.headers.authorization || '';
const currentUser = await getUserFromToken(token);
// Inicjalizacja DataLoaderów
const loaders = {
userById: new DataLoader(async (ids) => {
const users = await db.users.findByIds(ids);
return ids.map(id => users.find(u => u.id === id));
}),
postsByUserId: new DataLoader(async (userIds) => {
const posts = await db.posts.findByUserIds(userIds);
return userIds.map(userId =>
posts.filter(p => p.authorId === userId)
);
})
};
// Zwracany context dostępny w każdym resolverze
return {
db: database, // Połączenie z bazą
currentUser, // Zalogowany użytkownik
loaders, // DataLoadery dla batch loading
req, // Oryginalny request
logger: logger // Logger
};
}
});
// Przykład zaawansowanego użycia info do optymalizacji SQL
const userResolver = async (parent, args, context, info) => {
// Analiza żądanych pól
const fields = getDeepRequestedFields(info);
let query = 'SELECT ';
const columns = ['id', 'name', 'email'];
// Jeśli posts jest żądane, użyj JOIN
const postsField = fields.find(f => f.name === 'posts');
if (postsField) {
query += columns.join(', ') + ', posts.*';
query += ' FROM users LEFT JOIN posts ON users.id = posts.author_id';
} else {
query += columns.join(', ') + ' FROM users';
}
query += ' WHERE users.id = ?';
return await context.db.query(query, [args.id]);
};
Materiały:
Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziGraphQL - Subscriptions
Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziGraphQL - Wydajność i Optymalizacja
Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziGraphQL - Bezpieczeństwo
Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziGraphQL - Narzędzia i Ekosystem
Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedziOdpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.
Odblokuj odpowiedzi