GraphQL – definicja schematu

Mając za sobą teoretyczne wprowadzenie do języka zapytań GraphQL można przejść do omówienia najważniejszej części tego standardu, a mianowicie systemu typów, który pozwala na definiowanie schematu API – GraphQL Schema Definition Language (SDL). Jest to specjalna składnia, która została bardzo dobrze udokumentowana i stworzona w taki sposób aby można było z nią pracować niezależnie od języka bądź frameworka.

Type System

GraphQL jest silnie typowany co oznacza, że każda definicja musi posiadać jakiś określony typ. Z pomocą przychodzi Type System, który udostępnia kilka możliwości na definiowanie elementów naszego API. Będąc przy tematyce aplikacji związanej z książkami rozpoczętej przy okazji pierwszego posta zadeklarujmy pierwsze typy naszego API.

type Book {
  id: ID!
  title: String!
  shortDescription: String!
  description: String
  pages: Int!
  isbn: String!
  releaseDate: String!
  isReleased: Boolean!
  author: Author!
}

type Author {
  id: ID!
  name: String!
  bio: String
  birthday: String!
  sex: String!
  books: [Book!]!
}

Obiekt i pola

Najważniejszym i najczęściej wykorzystywanym elementem całej układanki jest Object Type, który w najprostszych słowach jest kolekcją pól. Biorąc pod lupę powyższe przykłady zadeklarowano tam dwa obiekty przy pomocy definicji type Book {} i type Author {}, z kolei wewnątrz tych deklaracji można zauważyć pola, które są określonych typów, np. name: String!, czy pages: Int!.

Typy skalarne

W GraphQL istnieje kilka wbudowanych typów skalarnych dla deklaracji pól:

  • String – zbiór znaków w formacie UTF-8
  • Int – 32-bitowa liczba całkowita
  • Float – liczba zmiennoprzecinkowa
  • Boolean – wartość true lub false
  • ID – typ reprezentujący unikalny identyfikator dla obiektu, najczęściej używany do ponownego pobrania (wykorzystywany przez cache). Serializowany jest w taki sam sposób jak typ String.

Interface

Interfejs w GraphQL w założeniu pełni rolę podobną do tej jak to ma miejsce w językach obiektowych, czyli wymusza pewien kontrakt. Obiekt, który implementuje dany interfejs musi zawierać pola, które zostały zadeklarowane w interfejsie.

Przykładowo do naszej aplikacji oprócz książek dodajemy obsługę magazynów. Możemy wymusić aby oba typy publikacji posiadały tytuł i datę wydania.

interface Publication {
  title: String!
  releasedDate: String!
}

type Magazine implements Publication {
  title: String!
  releasedDate: String!
  version: Int!
}

type Book implements Publication {
  title: String!
  releasedDate: String!
  pages: Int!
}

Bardzo prawdopodobne jest, że autor mógł wydać zarówno książki jak i magazyny, dzięki interfejsowi nie trzeba uzależniać się od konkretnego typu publikacji, w tym wypadku możemy użyć większej abstrakcji jaką jest Publication.

type Author {
  name: String!,
  publications: [Publication]
}

Union

Ciekawym mechanizmem jest Union type, który pozwala na reprezentowaniu grupy obiektów nie posiadających tych samych pól. Bardzo dobrym przykładem jest zapytanie do wyszukiwarki, która może przeszukiwać zarówno po tytule książki jak i imieniu autora.

union SearchResult = Book | Author

type Query {
  search(text: String!): SearchResult
}

Dzięki takiej deklaracji można wywołać mniej więcej takie zapytanie:

query {
  search(text: "test") {
    ... on Book {
      title
    }
    ... on Author {
      name
    }
  }
}

A w wyniku (jeśli zostaną znalezione odpowiednie rekordy) zobaczymy odpowiedź:

{
  "data": {
    "search": [
      {
        "name": "test book",
      },
      {
        "name": "test author",
      }
    ]
  }
}

Deklaracja schematu

Definiując schemat API dostępne są dwa elementy najwyższego poziomu – query oraz mutation, które są zwykłymi obiektami tworzone w taki sam sposób wszystkie inne. Wewnątrz nich deklarujemy możliwości naszego API. Definicja samego schematu jest banalna:

schema {
  query: Query
  mutation: Mutation
}

type Query {
}

type Mutation {
}

Query

Query jest obowiązkowym elementem w schemacie i odpowiada za odczyt API. Wszystkie definiowane pola wewnątrz tego obiektu można porównać do różnych endpointów API. Przyjętą zasadą jest, że elementy wystawiane za pośrednictwem query są rzeczownikami wprost określającymi encję, która ma być pobrana – w powyższym przykładzie są to book i author.

Aby lepiej zobrazować całość można przerzucić poprzednie definicje obiektów do query.

schema {
  query: Query
}

type Query {
  book(id: ID!): Book
  author(id: ID!): Author
}

type Book {
  id: ID!
  title: String!
  shortDescription: String!
  description: String
  pages: Int!
  isbn: String!
  releaseDate: String!
  isReleased: Boolean!
  author: Author!
}

type Author {
  id: ID!
  name: String!
  bio: String
  birthday: String!
  sex: String!
  books: [Book!]!
}

Argumenty

W liniach 6 i 7 można zauważyć deklarację pól nieco innych niż w poprzednich przypadkach (np. book(id: String!)), gdzie dodatkowo oprócz nazwy pola można zauważyć nawias z kolejną deklaracją – to nic innego jak wprowadzenie argumentu do zapytania – na jego podstawie można przekazać jakieś parametry według których chcemy pobrać dane. W powyższym przykładzie oczekiwany jest id użytkownika i wykonane zapytanie wyglądałoby mniej więcej tak:

query {
  book(id: "1234") {
    title
    isbn
  }
}

Mutation

Mutation jest opcjonalną częścią, która pozwala na dodawanie, edytowanie lub usuwanie elementów w naszej aplikacji za pośrednictwem API. Jego definicja jest identyczna do typu query. Jedyną różnicą jest zasada definiowania pól – w przeciwieństwie do query w mutation najczęściej pola nazywane są jako czasowniki, które jasno określają akcję jaka jest do wykonania. Uzupełniając powyższy przykład warto dodać możliwość tworzenia nowych książek.

Input Type

Zanim przejdziemy do przykładowego zadeklarowania mutation warto przedstawić jeszcze jeden typ pominięty podczas omawiania wszystkich podstawowych typów w podrozdziale, który dotyczył Type System.

Dla modyfikowania lub tworzenia nowych elementów w aplikacji za pośrednictwem GraphQL stworzono specjalny typ input, który zachowuje się bardzo podobnie jak zwykły obiekt, z tą różnicą, że podczas deklaracji zamiast słowa kluczowego type używane jest input.

schema {
  query: Query
  mutation: Mutation
}

type Mutation {
  createBook(input: BookInput): Book
  updateBook(id: ID!, input: BookInput): Book
}

input BookInput {
  title: String!
  pages: Int!
  isbn: String!
}

W powyższym przykładzie można zaobserwować, że akcje createBook i updateBook oczekują jako argument obiektu BookInput zaś zwracają obiekt Book. Dla tak zadeklarowanego schematu tworzenie nowej książki wymaga podobnej akcji:

mutation {
  createBook(input: {
    title: "Test book",
    pages: 512,
    isbn: "12211"
  }) {
    id
    title
  }
}

Podsumowanie

W dzisiejszym poście zostały omówione wszystkie najważniejsze i najbardziej podstawowe elementy języka zapytań GraphQL. Większość z tych elementów jest na tyle proste, że nie trzeba nawet się zastanawiać nad zasadą ich działania, takie też było główne założenie inżynierów Facebooka, którzy opracowywali ten standard – składnia ma być łatwa i czytelna.

NodeStart - Twórz back-end w JavaScript / TypeScript
Programista skupiony głównie wokół technologii webowych, ale nie przywiązujący się do konkretnych języków i narzędzi. Skoncentrowany na ciągłym rozwoju, zwolennik ruchu Software Crafmanship. Na codzień pracując w DAZN ma okazję rozwijać interesujący projekt do streamingu wydarzeń sportowych. Prywatnie fan sportu, a szczególnie piłki nożnej. Po godzinach próbuje również swoich sił w piwowarstwie domowym.
PODZIEL SIĘ