Strona główna / Artykuły

Przegląd kształtów prisma-guard: Właścicielstwo, projekcja i kontrakty zapisu

Naucz się analizować kształty prisma-guard jako umowy API, zadając pytania o to, kto jest właścicielem każdej wartości, jakie dane mogą znaleźć się w odpowiedzi oraz które zapisy generowanego punktu końcowego mogą zostać wykonywane.

Wydawca: REACTAPP.TOP12 min czytania2744 słów

Źródła: Define Every Prisma Endpoint’s Inputs and Outputs in One Place

Język argumentów Prismy jest wyrazisty, a w zaufanym kodzie serwera to dokładnie to, czego potrzebujesz. Jednak gdy ta sama struktura argumentów zostanie przyjęta przez HTTP, każda możliwość tego języka zamienia się w decyzję dotyczącą funkcjonalności produktu: co może napisać użytkownik, jakie filtry może wybrać, co serwer musi ustalić, które relacje mogą zostać zwrócone oraz ile wierszy potomnych może zawierać jedna odpowiedź. Formy prisma-guard umożliwiają zapisanie tych odpowiedzi bezpośrednio obok operacji Prisma. Ten przewodnik pokazuje, jak traktować formę jako zwięzłą umowę dotyczącą kontroli, filtrowania, projekcji i modyfikacji, wykorzystując trzy pytania, które można zadać przy każdym pull request dodającym generowany endpoint.

Dlaczego forma to coś więcej niż tylko walidacja

Kształt rzeczywiście weryfikuje dane wejściowe, ale jego większą zaletą jest to, że gromadzi decyzje dotyczące granic w jednym miejscu. Operacja generowana może korzystać z tego kształtu, zamiast aby każdy obsługujący element ręcznie odbudowywał schematy danych wejściowych oraz listy dopuszczalnych pól. Dzięki temu eliminuje się powtarzający się kod, a także ryzyko, że dwa obsługujące elementy nieświadomie będą się różnić w tym, co dozwalają.

Ceną tej wygody jest precyzja. Kształt stanowi wiarygodną umowę tylko wtedy, gdy odpowiada temu, co faktycznie egzekwuje biblioteka, a to obejmuje uczciwość co do tego, gdzie kończy się zastosowanie zakresu najwyższego użytkownika. Opisane poniżej zachowanie odzwierciedla implementację w takiej formie, w jakiej była testowana przez jej utrzymujących w momencie pisania, a nie ustalone konwencje społeczności, dlatego sprawdź szczegóły w dokumentacji swojej wersji. Jeśli zapytanie chronione już zawodzi i chcesz wiedzieć dlaczego, zacznij od naszego modelu diagnostycznego opartego na fazach dla błędów prisma-guard.

Pierwsze pytanie: kto jest właścicielem każdej wartości?

Każdy „liść” kształtu przekazuje wartość albo do klienta, albo na serwer. Słownictwo jest ograniczone; przykład pokazuje wszystkie cztery formy obok siebie.

const values = {
  clientChooses: true,
  serverChooses: 'published',
  serverChoosesTrue: force(true),
  clientChoosesWithinRule: (base) => base.min(1).max(120),
}

Czytaj uważnie opisy:

  • Samo true nie jest wartością logiczną. Jest to sygnał oznaczający „decyzja należy do klienta”.
  • Literał taki jak 'published' należy do serwera.
  • force(value) również należy do serwera. Jest potrzebny, gdy wartość, którą chcesz ustalić, to dosłownie true, ponieważ samo true zostałoby zinterpretowane jako sygnał klienta.
  • Funkcja udoskonalająca pozostawia decyzję klientowi, ale zastępuje wygenerowaną łańcuchową strukturę Zod pola regułą napisaną bezpośrednio.

Miejsce umieszczenia wpływa na znaczenie funkcji force

Słowa są proste; to ich lokalizacja zaskakuje ludzi. W kształcie where przymusowy skalar trafia do wnętrza obiektu operatora. Filtr zdarzeń poniżej fiksuje stan publikacji, pozwalając jednocześnie klientowi wyszukiwać tytuły:

const eventShape = {
  where: {
    isPublished: { equals: force(true) },
    title: { contains: true },
  },
}

Gdy całe predykat skalarne znajdujące się u korzenia where jest przymusowe, biblioteka traktuje dane wprowadzone przez klienta w tym polu łagodnie. Niezależnie od tego, czy użytkownik powtórzy ustaloną wartość, wyśle inną lub doda inny operator, predykat serwera ma pierwszeństwo i żądanie nie jest odrzucane tylko dlatego, że zostało to pole wspomniane. To nie jest to samo co domyślna wartość – domyślna zostaje pominięta, gdy klient dostarcza jakieś dane; przymus natomiast nigdy nie ustępuje.

We wszystkich innych pozycjach zasady są ścisłe. Wyobraź sobie modyfikator przymuszony umieszczony obok operatora kontrolowanego przez klienta, pole przytwierdzone w filtrze relacji, filtr przytwierdzony do zagnieżdżonego elementu include lub wartość przytwierdzoną w danych mutacji. We wszystkich tych przypadkach klucz znika ze schematu, według którego sprawdzana jest poprawność danych klienta, więc jego wysłanie powoduje niepowodzenie weryfikacji, nawet jeśli wartość jest dokładnie taka sama.

To ma znaczenie dla kodu klienta. Klient może być zmuszony całkowicie usunąć pole do zapisu należące do serwera. „Poprawienie” jego wartości na wartość serwera nadal nie przynosi skutku, ponieważ sprawdzenie dotyczy tego, kto posiada klucz, a nie tego, czy wartości są równe.

Domyślne ustawienia, obsługa błędów i udoskonalenia również zmieniają właścicielstwo

Wartości domyślne i mechanizmy obsługi błędów zasługują na takie samo badanie. Na polu kontrolowanym przez klienta wartość domyślna zachowuje się normalnie, gdy jej nie podano, a mechanizm obsługi błędów może zastąpić nieprawidłowy wpis wartością awaryjną, zamiast odrzucić żądanie. Operacje tworzenia dodają dodatkową złożoność: dyrektywa domyślna lub obsługi błędów wygenerowana przez generator może nawet wypełnić pole obowiązkowe, o którym w specyfikacji nie ma mowy, a wygenerowana wartość jest następnie wprowadzana tak, jakby serwer ją ustalił. Dlatego to, gdzie umieścimy wartość domyślną, decyduje o tym, kto jest właścicielem tej wartości – więc polegajmy na substytucji tylko wtedy, gdy tego wymaga umowa API.

Udoskonalenia kryją w sobie powiązaną pułapkę. Nie rozszerzają generowanej łańcuchowej struktury w swoim węźle – zastępują ją. Dlatego funkcja musi ponownie określić każde ograniczenie, od którego zależy punkt końcowy, a nie tylko dodatkowe wartości minimalne lub maksymalne, które chcieliśmy dodać. Jest to szczególnie ważne, gdy typ w Prismie jest bardziej swobodny niż reguła iloczynowa, na przykład kolumna tekstowa, która nigdy nie może być pusta.

Zmuszone aktualizacje i zmuszone dysjunkcje

Dane aktualizacyjne są zapisywane przy każdej dopasowanej wywołaniu. Załóżmy, że struktura aktualizacji wymusza wartość status: 'active': każda aktualizacja przeprowadzona za jej pośrednictwem, nawet ta, która edytuje tylko tytuł, również resetuje status. Jeśli operacja musi pozostawić pole nietknięte, należy je wykluczyć ze struktury aktualizacji.

Struktura boolowska dodaje jeszcze jedną zasadę. Warunek przymusowy umieszczony pod OR jest przenoszony na poziom warunku obowiązkowego u korzenia. W rezultacie forma nie ma możliwości wyrażenia prawdziwej zasady „albo… albo” należącej do serwera, takiej jak rekordy należące do tego konta lub te udostępnione z tą grupą. Takie zasady wymagają zapytania napisanego na serwerze, pojedynczej kolumny określającej zakres, która przechowuje zasady, lub egzekwowania w samej bazie danych.

Pierwsze pytanie do przeglądu jest zatem bardziej precyzyjne niż „czy to zostało zweryfikowane?”. Należy zapytać, kto decyduje o każdej wartości, gdzie umieszczane są wszystkie przymusy oraz jaki drzewo argumentów faktycznie trafia do Prismy.

Druge pytanie: co może ujawnić odpowiedź?

Filtrowanie i projekcja odpowiadają na różne pytania. Filtr najemcy decyduje, które rekordy spełniają kryteria. Projekcja określa, które z ich pól i relacji mają zostać odesłane. Potrzebne są obie, a sprawdzenie jednej nic nie mówi o drugiej.

W przypadku chronionych odczytów, instrukcja select lub include zadeklarowana dla danej struktury pełni jednocześnie rolę listy dozwolonych elementów oraz rozwiązania awaryjnego. Klienci mogą żądać węższej struktury, ale nic poza nią, a gdy nie przesyłają żadnej projekcji, delegat chroniony używa tej zadeklarowanej.

Poniższa struktura biletu umożliwia klientowi pobranie biletu podając jego ID, zwraca ustaloną liczbę pól korzeniowych oraz udostępnia zagnieżdżoną listę notatek klienta wraz z własnym filtrem, polami i rozmiarem strony.

const ticketShape = {
  where: {
    id: { equals: true },
  },
  select: {
    id: true,
    subject: true,
    description: true,
    customerNotes: {
      where: {
        accountId: { equals: force(accountId) },
      },
      select: {
        id: true,
        body: true,
      },
      take: { max: 20, default: 10 },
    },
  },
}

Ta jedna struktura wykonywa cztery oddzielne zadania:

  • Pola korzeniowe, które nie są wymienione, pozostają niedostępne.
  • Tylko wymienione pola notatek mogą zostać zwrócone.
  • Notatki są zawsze ograniczone do konta dzwoniącego.
  • Jedna odpowiedź może zawierać maksymalnie 20 notatek, a 10 w przypadku braku określenia przez klienta.
  • Związki wewnętrzne znajdują się poza automatycznym zakresem

    Filtr notatek został celowo tak zaprojektowany. Automatyczne określanie zakresu dotyczy wyłącznie operacji obsługiwanych na poziomie korzenia zapytania; nie obejmuje ono związków pobranych za pomocą select lub include, ani nie dodaje tam żadnych warunków. Można zapomnieć o zagnieżdżonych klauzulach where – poprawnie skonfigurowane zapytanie nadal może zwracać notatki należące do innego konta.

    Relacje typu „wiele-do-wielu” przyjmują zagnieżdżony warunek where, własną projekcję oraz ograniczenie dotyczące wierszy. Relacje typu „jedno-do-jednego” nie mogą używać zagnieżdżonego warunku where w Prismie, więc potrzebna jest inna strategia: pominąć tę relację, udostępnić tylko nieszkodliwe pola skalarne pochodzące z niej, załadować ją za pomocą jej własnego zabezpieczonego zapytania na poziomie korzenia lub zabezpieczyć ją za pomocą polityki bazy danych.

    Wybór stylu projekcji

    Wybierz formę projekcji w zależności od tego, jak będzie się ona zachowywać w miarę rozwoju schematu:

    • Z użyciem select kolumna dodana później do modelu pozostaje ukryta, dopóki ktoś jej nie przejrzy i nie doda do listy, co czyni to najbezpieczniejszym wyborem dla publicznych punktów końcowych.
    • Z użyciem include uzyskujesz relacje, ale model korzeniowy nadal zwraca wszystkie swoje pola skalarne, więc wrażliwe dane korzeniowe pozostają widoczne.
  • Za pomocą omit utrzymujesz listę wykluczeń, a to, czy jest dostępna, zależy od twojego generatora oraz wersji Prismy.
  • Są to alternatywy, a nie warstwy. Nigdy nie łączaj select, include i omit na jednym poziomie drzewa.

    Liczby, ustawienia domyślne w nawiasach i odpowiedzi na mutacje

    Liczba relacji to odrębny węzeł projekcji, a nie podsumowanie powiązane z załadowaną relacją. Jeśli relacja typu „do-wielu” jest filtrowana według konta, jej _count musi zawierać ten sam filtr, gdyż klienci zakładają, że obie liczby opisują te same wiersze. Testuj warunek generowanej relacji oraz warunek liczby niezależnie od siebie.

    Standardowe ustawienia zagnieżdżonej projekcji pochodzą od deklarowanej struktury. Jeśli klient poprosi o zadeklarowaną relację w formie skrótu logicznego, relacja może nadal zachować swój ustalony filtr i ograniczenia strony. Uprawnienia przyznane klientowi jednak nie uzupełniają brakujących opcji: umożliwienie sortowania według pola nie określa kierunku, który klient pominął. W testach należy przetestować trzy przypadki: pominiętą projekcję korzeniową, wyraźną zagnieżdżoną projekcję oraz skrót relacji, zamiast zakładać, że jeden przypadek dowodzi reszty.

    Mutacje są celowo asymetryczne. Kształt mutacji może sprawdzić projekcję, o którą prosi klient, ale jeśli nie jest włączone enforceProjection, klient, który o nic nie prosi, nie otrzymuje żadnej sformatowanej projekcji i obowiązuje standardowe zachowanie Prismy. Włącz tę funkcję wtedy, gdy odpowiedzi na mutacje muszą być ograniczone do zadeklarowanych pól.

    Na koniec upewnij się, że każdy test uruchamia tę fazę, którą rzekomo sprawdza. Wywołanie guard.query(...).parse() to dobry sposób na sprawdzenie filtrów odczytu i ograniczeń, ale domyślne projekcje odczytu pojawiają się dopiero wtedy, gdy uruchomiony zostanie zabezpieczony delegat, a podobnie projekcje mutacji wymagają ścieżki mutacji zabezpieczonej.

    Zatem drugie pytanie brzmi nie „czy istnieje select?”, lecz jakie pola korzeniowe, powiązane pola, powiązane wiersze oraz liczby może otrzymać każda wersja wywołująca.

    Pytanie trzecie: jakie operacje zapisu mogą zostać wykonane?

    Generowane punkty końcowe do zapisu przyjmują argumenty w własnym formacie Prismy. Kształt danych określa, jakie wartości dostarcza klient, a jakie serwer, oraz czy możliwe są jakiekolwiek zagnieżdżone operacje.

    Kształty tworzenia muszą uwzględniać wszystkie wymagane pola

    Kształt tworzenia zostaje odrzucony, jeśli jakieś wymagane pole nie ma źródła. Do dopuszczalnych źródeł należą:

    • klient, gdy pole jest oznaczone jako kontrolowane przez klienta
    • serwer, poprzez wartość literalną lub metodę force
    • wartość domyślna zdefiniowana w schemacie Prisma
    • wartość domyślna lub mechanizm obsługi Zod
    • wstrzyknięcie klucza obcego tenanta w kontekście operacji
    • zapis relacji dozwolonej, który ją wypełnia

    Przykład subskrypcji umożliwia klientowi wybór zewnętrznego klucza i planu, ogranicza liczbę miejsc za pomocą reguły całkowitej, ustala status i wersję początkową na serwerze oraz zwraca skrócony wynik.

    const createSubscriptionShape = {
      data: {
        externalKey: (base) => base.min(1).max(100),
        planId: true,
        status: 'trialing',
        seatCount: (base) => base.int().min(1).max(500),
        version: 0,
      },
      select: {
        id: true,
        externalKey: true,
        status: true,
        seatCount: true,
      },
    }
    

    Zauważ, czego brakuje: klucza obcego tenanta. Nigdy nie powinien on być ustawiany przez klienta podczas tworzenia. Korzeń kontekstu operacji pochodzi z zaufanego kontekstu żądania, a rozszerzenie dodaje odpowiedni klucz obcy dla zapisów na poziomie korzenia, które obsługuje.

    Operacje masowe i tworzenie partiami

    Mutacje masowe muszą przejść dwie weryfikacje. Po pierwsze, updateMany, jego zwracane warianty oraz deleteMany wymagają struktury umożliwiającej określenie warunków za pomocą klauzuli where. Po drugie, przychodząca prośba musi zawierać rzeczywisty warunek. Brak ciała żądania, pusty obiekt operatora lub gałąź logiczna pasująca do wszystkiego nie spełnia wymagań, co zapobiega przypadkowym zmianom w całej tabeli.

    Funkcja obsługująca operacje masowe wymaga tablicy i nie pakuje w tajemnicy pojedynczego obiektu. To, czy zwracane operacje masowe oraz opcje takie jak pomijanie duplikatów nadal działają, zależy od dostawcy bazy danych.

    Upsert to trzy kontrakty

    Struktura upsertu składa się właściwie z trzech elementów: unikalnego selektora, danych do utworzenia rekordu oraz danych do jego aktualizacji.

    const upsertSubscriptionShape = {
      where: {
        externalKey: true,
      },
      create: {
        externalKey: true,
        planId: true,
        status: 'trialing',
        seatCount: true,
        version: 0,
      },
      update: {
        planId: true,
        seatCount: true,
      },
    }
    

    Każde pole wymagane musi być uwzględnione w treści operacji create, natomiast w treści operacji update można pominąć cokolwiek. Parametr force w metodzie create ustawia wartość tylko przy dodawaniu nowej wiersza, podczas gdy ten sam parametr w metodzie update nadpisuje tę wartość za każdym razem, gdy istniejący wiersz się pokrywa. W przykładzie status i wersja są określone podczas operacji create, a celowo pomijane przy update, dzięki czemu istniejące subskrypcje zachowują swoje obecne ustawienia.

    Jedynostkowe modyfikacje przyjmują prostą formę unikalnego wybieracza w Prismie. Obiekty operatorów służą do zwykłego filtrowania i nie mają miejsca w strukturze unikalnego warunku where na poziomie korzenia.

    Kontrakty zapisu i zwrotu relacji

    Relacja wymaga odrębnej decyzji dotyczącej własności. Kształt może zezwalać na connect, ograniczać klawisze selektora, których może używać klient, oraz odrzucać wszelkie operacje wewnętrzne, które nie są wymienione. Nic z tego jednak nie rozszerza zakresu użytkownika na selektor. Gdy ma znaczenie, który rekord zostanie połączony, kod aplikacji lub baza danych muszą sprawdzić, czy osoba wywołująca ma na to pozwolenie.

    Umowy zwrotu również różnią się w zależności od metody. Metody batch służące wyłącznie do liczenia, createMany, updateMany i deleteMany, odpowiadają tylko liczbą rekordów i odrzucają projekcje. Ich odpowiedniki zwracające dane mogą przyjmować projekcje, jeśli zarówno Prisma, jak i dostawca umożliwiają to. Jeśli ustawienie generatora zmienia trasę na metodę zwracającą dane, należy przeanalizować nową odpowiedź jako zmianę API, a nie odrzucić ją jako szczegół implementacji.

    Trzecie pytanie to nie „czy ta metoda jest włączona?”. Należy zapytać, co daje dostęp taka metoda: pola danych, formularze selektorów, warunki masowe, operacje relacyjne oraz pola odpowiedzi.

    Kładzenie warstw wdrożenia

    Formy funkcjonują najlepiej, gdy ich ograniczenia są udokumentowane obok innych mechanizmów. Każda warstwa ma swoje zadanie:

    • Zautoryzowany żądanie dostarcza wiarygodnego kontekstu.
    • Mapowania zakresów dodają warunki użytkowników do obsługiwanych operacji na poziomie korzenia.
    • Warianty wywołujące decydują, która zadeklarowana forma ma zastosowanie do określonej grupy odbiorców.
    • Haki uruchamiają ograniczone sprawdzenia aplikacji, które mogą odrzucić żądanie.
    • Ręcznie napisane obsługi zajmują się zapytaniami, których formy nie mogą opisać.
    • Polityka bazy danych chroni niezmienniki, które muszą przetrwać każdą ścieżkę dostępu.

    Żaden z tych elementów nie powinien udawać, że wykonuje pracę innego. Jeśli decydujesz, które ścieżki mają być generowane, a które wpisywane ręcznie, nasz artykuł na temat zrównoważenia generowania danych Prisma CRUD z celowym kontrolowaniem ścieżek omawia tę kwestię.

    Przegląd i lista kontrolna testów

    Gdy prośba o połączenie dodaje zgenerowany punkt końcowy, przeanalizuj ją, zadając sobie trzy pytania:

    1. Kto decyduje o każdej wartości, którą może przyjąć punkt końcowy?
    2. Które pola i relacje mogą znajdować się w odpowiedzi?
    3. Które mutacje i formularze selektorów może wykonać użytkownik?

    Następnie każdy test powinien być skierowany na konkretną tezę, którą ma udowodnić. Analizuj argumenty wejściowe, aby potwierdzić zachowanie filtrów. Uruchamiaj zabezpieczone delegaty w celu sprawdzenia operacji projekcji i zapisu. Sprawdzaj ścieżkę rozszerzenia, by potwierdzić iniekcję zakresu. Używaj przykładowych danych bazy danych wieloświadczeniowych, gdy chodzi o to, które wiersze zostaną zwrócone, a przy testach HTTP – gdy chodzi o transport lub generowane ścieżki.

    Nadawaj testom negatywnym takie samo znaczenie. Dla każdej grupy docelowej spróbuj odczytać pole korzeniowe, które nie jest wymienione, odczytać wrażliwe pole zagnieżdżone, przekroczyć ograniczenie relacji oraz wykonać operację zapisu, której forma jest zabroniona. Testy pozytywne potwierdzają, że zamierzona prośba zostaje zrealizowana; testy negatywne pokazują, że granice pozostają zamknięte po zmianach w modelu lub generatorze.

    Zakończenie

    Kształt otrzymuje nazwę „contract” tylko wtedy, gdy jest jasne, co może realizować, a czego nie może wymusić. Oddzielna analiza prawa własności, projekcji oraz operacji zapisu pozwala wcześnie wykryć typowe luki: łagodne zasady na poziomie korzenia, nieskalowane powiązania wewnątrz siebie, liczniki ignorujące filtry relacji oraz połączenia omijające zakres użytkownika. Gdy te luki zostaną przypisane do odpowiednich mechanizmów, obsługi lub zasad bazy danych, generowanie kodu może usunąć powtarzający się kod graniczny, nie zamieniając przy tym języka zapytań Prisma w publiczną API, której nikt nie zamierzał publikować. Pełną składnię, dokładne komunikaty o błędach, zachowanie specyficzne dla wersji oraz przykłady testów można znaleźć w przewodniku prisma-generator-express.

    Literatura pokrewna