Pytania rekrutacyjne GraphQL

38 pytań — 20 odpowiedzi dostępnych od razu

Przygotuj się do rozmowy rekrutacyjnej z GraphQL, sprawdzając darmowy podgląd przykładowych pytań i odpowiedzi. Materiał pochodzi z zestawu 38 zagadnień i pokazuje formę, w jakiej możesz powtarzać kluczowe pojęcia.

Zakres obejmuje podstawy GraphQL, system typów, query i mutation, resolvery oraz subskrypcje. Pytania pomagają uporządkować różnice między warstwą API, modelem danych i sposobem obsługi żądań.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 38 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 18 odpowiedzi

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:

  1. Over-fetching - w REST często pobieramy więcej danych niż potrzebujemy, bo endpoint zwraca całą strukturę
  2. Under-fetching - potrzeba wykonania wielu requestów do różnych endpointów, aby zebrać wszystkie potrzebne dane
  3. Wersjonowanie API - GraphQL pozwala ewoluować API bez konieczności tworzenia nowych wersji
  4. 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:

  1. Typy obiektów - definiują strukturę danych (np. User, Post, Comment)
  2. Pola - właściwości obiektów z określonym typem
  3. Root types - Query, Mutation, Subscription - punkty wejścia do API
  4. Skalary - podstawowe typy (String, Int, Float, Boolean, ID)
  5. Enums - wyliczenia możliwych wartości
  6. Interfaces i Unions - abstrakcje dla polimorfizmu
  7. 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:

  1. Automatyczna dokumentacja - narzędzia jak GraphiQL, GraphQL Playground, Apollo Studio wykorzystują introspection do generowania interaktywnej dokumentacji
  2. Type generation - generowanie typów TypeScript/Flow na podstawie schematu
  3. Walidacja zapytań - IDE i editory mogą walidować zapytania w czasie pisania
  4. Autocomplete - inteligentne podpowiedzi podczas pisania zapytań
  5. 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:

  1. Named Fragments - nazwane fragmenty, które można reużywać w wielu zapytaniach
  2. 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:

21. 21. Jak obsługiwać błędy w resolverach GraphQL?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. 22. Czym jest kontekst (context) i jak go wykorzystać do współdzielenia danych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

GraphQL - Subscriptions

23. Jak działają Subscriptions w GraphQL i do czego służą?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
24. Jakie protokoły są używane do implementacji Subscriptions?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
25. Jak zaimplementować real-time updates używając GraphQL Subscriptions?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

GraphQL - Wydajność i Optymalizacja

26. 26. Czym jest problem N+1 w GraphQL i jak go rozwiązać?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. 27. Jak działa DataLoader i dlaczego jest kluczowy dla wydajności?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
28. 28. Jakie strategie cachowania można stosować w GraphQL?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
29. 29. Czym jest persisted queries i jakie daje korzyści?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. 30. Jak ograniczyć złożoność zapytań (query complexity)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

GraphQL - Bezpieczeństwo

31. Jakie są główne zagrożenia bezpieczeństwa w GraphQL API?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
32. Jak implementować autoryzację i uwierzytelnianie w GraphQL?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
33. Jak chronić API przed zbyt złożonymi lub głębokimi zapytaniami?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
34. Czym jest rate limiting w kontekście GraphQL?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

GraphQL - Narzędzia i Ekosystem

35. Jakie są popularne biblioteki do budowania GraphQL API (Apollo, Yoga, etc.)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
36. Jak działa Apollo Client i jakie oferuje funkcjonalności?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
37. Czym jest GraphQL Code Generator i jak przyspiesza development?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Jak testować API GraphQL i jakich narzędzi używać?

Odpowiedź dostępna w pełnej wersji. Odblokuj 18 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 38 pytań rekrutacyjnych z GraphQL oraz pozostałych technologii.

Zobacz plany cenowe