Blog

🤔 Czy GraphQL Powinno Być Różne dla Różnych Użytkowników?

Leonardo Losoviz
Autor: Leonardo Losoviz ·

GraphQL to interfejs do pobierania danych z pewnego źródła, przy czym specyfikacja GraphQL definiuje wymagania dla tego interfejsu. Dopóki te wymagania są spełnione, GraphQL nie interesuje się tym, jak to jest osiągane. Serwer GraphQL może być zaimplementowany w JavaScript przy użyciu promises, za pomocą architektury współbieżnej opartej na Golang, zmapowany do pliku Excel lub cokolwiek innego — a wszystkie te implementacje mogą być prawidłowymi realizacjami specyfikacji GraphQL.

GraphQL stoi pomiędzy klientem a usługami backendowymi

Sposób implementacji silnika serwera nie jest istotny dla pomyślnego wykonania żądania GraphQL, ponieważ interakcja między klientem a serwerem jest zawsze taka sama — polega na wysłaniu query GraphQL przy użyciu zdefiniowanej składni i uzyskaniu odpowiedniej odpowiedzi w formacie JSON.

Teraz, gdy mówię, że implementacja nie jest ważna, mam na myśli perspektywę użytkownika API, który po prostu zamierza pobrać dane z serwera. To, jak zostały wyprodukowane zwrócone dane, nie jest przedmiotem zainteresowania.

Ale sytuacja zmienia się dla programisty po stronie serwera pracującego nad API, dla którego szczegóły implementacji są bardzo istotne. Jeśli koduję swoje API GraphQL w PHP, to zrobię co w mojej mocy, aby moje API było rozwiązywane jak najefektywniej i miało jak najbardziej elegancki projekt architektoniczny, korzystając z możliwości oferowanych przez PHP.

PHP vs Java vs JavaScript

Mamy więc potencjalny konflikt interesów pomiędzy koniecznością zabezpieczenia API a oczekiwanymi możliwościami programistów pracujących nad API, którzy nie chcą, aby odbierano im funkcje obsługiwane przez bazowy język (takie jak możliwość wykonywania kodu rekurencyjnego).

Ten konflikt stał się widoczny w zgłoszeniu #929: Allow recursive references in fragments, które argumentuje, że GraphQL nie powinien zakazywać rekurencji w fragmentach.

Na poprzednim meetupie grupy roboczej GraphQL, Roman — programista, który zgłosił ten problem — wyraził, dlaczego jest w niezgodzie z ograniczeniem narzuconym przez specyfikację:

Jestem programistą po stronie serwera i czuję, że specyfikacja za dużo mówi o wykonaniu po stronie serwera, podczas gdy powinna skupiać się na tym, co klient chce otrzymać — nie na tym, jak

Reguła zakazująca rekurencji w fragmentach została uzasadniona przesłanką utrzymania bezpieczeństwa publicznego API. W końcu GraphQL został stworzony przez Facebook, aby dostarczać dane do jego aplikacji skierowanej do użytkowników publicznych, i użytkownicy nie powinni mieć możliwości wykorzystania luki w projekcie API, która mogłaby obalić usługę.

Twórca GraphQL, Lee Byron, wyraził trzy główne obawy:

nieskończona rekurencja; ograniczenia nie byłyby tylko specyfikacją — jak i kiedy powinna się zatrzymać

walidacja danych; zwracanie tej samej wartości wielokrotnie — jak jest to reprezentowane w danych. Idealnie chcesz wykryć, że jest cykliczna, i natychmiast zatrzymać, ale niektóre serwery nie mogą tego wykryć i mogą wykonywać pętlę wiele razy, zanim wykryją, że coś poszło nie tak, i zatrzymają się

jaki jest koszt braku tego; czy uzasadnia te problemy? Nie; zawsze jest możliwe określenie liczby poziomów głębokości w swojej query — to jest efektywnie odcukrzona wersja tego, co zrobilibyśmy, gdybyśmy obsługiwali to w GraphQL

Wychodząc ze swoich własnych perspektyw, zarówno Roman, jak i Lee mają rację. Lee Byron martwi się o bezpieczeństwo publicznego API GraphQL. Unikanie rekurencyjnych fragmentów jest uzasadnione, aby upewnić się, że żaden złośliwy aktor nie może obalić systemu, wykonując nigdy niekończącą się pętlę cykliczną w query, a nawet wyeliminować ryzyko "self-DDoSing" zespołu, co mogłoby się zdarzyć, gdyby nieumyślnie opublikowali query, która zawiesza system.

Roman natomiast jest zaniepokojony ograniczeniami swoich własnych możliwości tworzenia API GraphQL. Ponieważ Roman może być jedynym konsumentem swojego API (tj. prywatne API, które nie jest udostępniane użytkownikom) lub ponieważ jego serwer może mieć możliwość wykrywania i zatrzymywania powtarzających się cykli, uważa, że ograniczenie GraphQL jest szkodliwe i nieuzasadnione.

W sercu dyskusji problem nie polega na tym, czy rekurencyjne fragmenty powinny być dozwolone, czy nie, ale na czymś bardziej fundamentalnym: Kto jest docelową grupą GraphQL? Jeśli nie jest to jedna grupa, czy pojedyncza specyfikacja API mogłaby spełnić wymagania wszystkich różnych interesariuszy? A jeśli nie można zapobiec konfliktowi, czy przynajmniej można go w jakiś sposób złagodzić?

Zbadajmy te pytania.

Kto jest docelową grupą GraphQL?

GraphQL jest używany przez różne rodzaje interesariuszy, wśród których możemy zidentyfikować:

1. Użytkownicy API: Ci, którzy pobierają dane z jakiegoś endpointu GraphQL, z dowolnego powodu. Na przykład, wszyscy możemy być użytkownikami publicznego API GraphQL GitHub, aby pobierać dane dotyczące naszych repozytoriów GitHub.

2. Programiści po stronie klienta: Ci, którzy tworzą aplikacje po stronie klienta zasilane przez jakiś endpoint GraphQL. Na przykład, programiści budujący strony z Gatsby polegają na GraphQL do pobierania treści strony.

3. Programiści backend: Ci, którzy tworzą resolvery dla API GraphQL.

Ponadto musimy zauważyć, że API GraphQL może być publiczne lub prywatne:

API publiczne: Ponieważ każdy ma dostęp do endpointu GraphQL, musimy się martwić o środki bezpieczeństwa, aby uniknąć ataków ze strony złośliwych aktorów.

API prywatne: Ponieważ tylko uprawnione podmioty mają dostęp do API, nie ma nieodłącznych zagrożeń bezpieczeństwa, a self-DDoSing można łatwo uniknąć dzięki dobrym praktykom kodowania.

Czy pojedyncza specyfikacja API spełnia wymagania wszystkich interesariuszy?

Problem zgłoszony przez Romana można zinterpretować w ten sposób: "Jeśli moje API GraphQL jest prywatne i wiem dokładnie, co robię (mając 100% pewności, że mój kod zadziała zgodnie z oczekiwaniami i nie zostaną wyprodukowane żadne zawieszone wykonania), to dlaczego nie mogę używać rekurencji w fragmentach?"

Rekurencje @ xkcd

Przykład tej sytuacji ma miejsce zawsze, gdy używamy frameworka opartego na GraphQL do budowania statycznych stron (takich jak Gatsby, Next.js lub RedwoodJS), ponieważ API GraphQL często będzie prywatne i nie możemy nieumyślnie przeprowadzić ataku DDoS na naszą aplikację i ponieść niekorzystnych konsekwencji (co najwyżej zawiesi się podczas budowania statycznej strony w środowisku deweloperskim lub staging).

Programiści korzystający z powyższej konfiguracji mogą doskonale zastanawiać się, dlaczego specyfikacja GraphQL zakazuje im korzystania z korzystnych funkcji, które nie mają żadnych negatywnych konsekwencji dla ich konfiguracji.

Podsumowując, zakazując rekurencyjnych fragmentów, specyfikacja GraphQL narzuca środek bezpieczeństwa, który odnosi się do określonej grupy wszystkich potencjalnych zastosowań GraphQL, a nie do wszystkich, aby być po bezpiecznej stronie.

Czy specyfikacja GraphQL mogłaby lepiej zadowolić wszystkich interesariuszy?

Jeśli różni interesariusze mają różne wymagania, jak specyfikacja GraphQL może spełnić wszystkie z nich? (Chodzi o to, aby uniknąć forkowania specyfikacji i tworzenia niestandardowych wersji dla konkretnych grup docelowych.)

Zbadajmy kilka pomysłów, gdzie pierwszy wymagałby przejścia przez proces wnoszenia wkładu do specyfikacji, a drugi nie.

Feature-toggle na poziomie specyfikacji GraphQL

Jedną z możliwych dróg jest sprawienie, aby specyfikacja "sugerowała", a nie "narzucała" zasady. W tym przypadku reguła zakazująca rekurencji w fragmentach mogłaby być silnie sugerowana, ale funkcja byłaby nadal akceptowana.

Teraz to rozwiązanie zmieniłoby domyślny stan rekurencyjnych fragmentów z "obowiązkowego" na "opcjonalny", co produkowałoby dwa negatywne skutki:

  • API byłoby domyślnie niebezpieczne (scenariusz, którego Lee Byron chce uniknąć)
  • Produkowałoby to zmianę łamiącą kompatybilność, ponieważ zakazana query stałaby się dozwolona

Dlatego lepiej byłoby odwrócić opcję, pozostawiając rekurencje w fragmentach nadal domyślnie zakazane, ale dając możliwość przełączenia flagi funkcji, która wyłącza to zachowanie. Ponieważ funkcja musi być jawnie wyłączona, zrobią to tylko administratorzy wiedzący, co robią.

Ponieważ funkcja jest najbardziej wartościowa w określonych konfiguracjach, serwery i frameworki GraphQL mogłyby zdecydować, czy/jak/kiedy oferować tę konfigurację. Na przykład Gatsby mógłby wyraźnie wyświetlać tę opcję za pomocą jakiegoś interfejsu użytkownika podczas tworzenia statycznych stron i ukrywać ją w innym przypadku.

Ogólna idea polega na tym, aby specyfikacja GraphQL obsługiwała "włączone, ale opcjonalne funkcje", które można włączać/wyłączać poprzez konfigurację, a ich domyślny stan jest tym, który już mają w specyfikacji.

Zakazanie rekurencyjnych fragmentów byłoby jedną z nich, i mogłyby być inne takie funkcje, jak typ Map, który nie został zaakceptowany do specyfikacji przez Lee Byrona ponieważ:

Istnieją znaczące kompromisy między typem Map a listą par klucz/wartość. Jednym problemem jest paginacja kolekcji. Listy wartości mogą mieć jasne reguły paginacji, podczas gdy Maps, które często mają nieuporządkowane pary klucz-wartość, są znacznie trudniejsze do paginacji.

Innym problemem jest użycie. Najczęściej Map jest używany w API, gdzie jedno pole wartości jest indeksowane, co moim zdaniem jest antywzorcem API, ponieważ indeksowanie jest kwestią przechowywania i kwestią pamięci podręcznej klienta, a nie kwestią transportu. Ten antywzorzec mnie niepokoi. Chociaż istnieją dobre zastosowania dla Maps w API, obawiam się, że powszechne użycie będzie dla tych antywzorców, więc sugeruję zachowanie ostrożności.

Lee Byron wyraził swoje obawy, że funkcja będzie używana jako antywzorzec. Jednak przyznał również, że istnieją dla niej dobre zastosowania. Następnie, ponieważ zgłoszenie zebrało sporo wsparcia społeczności (z ponad 150 👍), programiści mogli mieć możliwość jawnego włączenia dodania typu Map do swoich schematów i radzenia sobie z konsekwencjami.

Feature-toggle przez serwery GraphQL

Jeśli powyższa propozycja nie zbierze wsparcia, ponieważ jest zbyt ryzykowna dla specyfikacji GraphQL, alternatywą jest jej implementacja na poziomie serwera GraphQL. Wtedy serwery GraphQL mogłyby zapewnić niestandardową funkcję, która wyłącza rekurencje w fragmentach.

Uogólniając ten pomysł, serwery GraphQL mogłyby oferować możliwość wyłączania pewnych funkcji ze specyfikacji i włączania innych, których brakuje w specyfikacji. Aby to zachowanie nie powodowało niespodzianek, serwery muszą upewnić się, że domyślny stan jest tym wymaganym przez specyfikację, a administrator API musi być w pełni świadomy konsekwencji przełączania funkcji. (Jest to strategia stosowana przez Gato GraphQL dla jego "innovative features".)

Podsumowanie

W miarę jak GraphQL stawał się coraz bardziej popularny, nowe frameworki obsługujące nowe możliwości uczyniły go częścią swoich stosów, a nowi interesariusze (i nowe ich typy) zaangażowali się. Zatem specyfikacja początkowo stworzona przez Facebook, aby zdefiniować, jak jego aplikacje będą pobierać dane z jego serwerów, musi coraz bardziej zmagać się z większą liczbą przypadków użycia.

Nieuniknione jest powstawanie konfliktów, gdzie pewna grupa interesariuszy potrzebuje funkcji, która jest kontrproduktywna lub nawet szkodliwa dla innych interesariuszy, jak ma to miejsce w przypadku rekurencyjnych fragmentów. Co można zrobić, aby poprawić sytuację i uniknąć, że niezadowoleni interesariusze nie rozczarują się GraphQL?

Argumentowałem, że specyfikacja mogłaby oferować możliwość "wyłączenia" funkcji, pozwalając administratorom wiedzącym, co robią, usunąć pewne ograniczenia, aby spełnić własne wymagania. Teraz, sam nie zgadzam się z tym rozwiązaniem, ale mimo wszystko wychodzę z nim na otwartą scenę, ponieważ ta dyskusja musi się odbyć. Ponieważ ten pomysł jest kontrowersyjny, lepszą alternatywą jest to, aby serwery GraphQL zapewniały to zachowanie za pomocą niestandardowych funkcji, które muszą być jawnie włączone.


Zapisz się do naszego newslettera

Bądź na bieżąco ze wszystkimi aktualizacjami Gato GraphQL.