Strona główna / Artykuły / Zrównoważenie generowania routerów Prisma CRUD z celowym kontrolowaniem tras.

Zrównoważenie generowania routerów Prisma CRUD z celowym kontrolowaniem tras.

Dowiedz się, jak generowanie routerów Prisma CRUD oparte na schematach może eliminować powtarzalne fragmenty kodu szablonowego, zachowując jednocześnie decyzje dotyczące zaufania, zakresu działania i dostępu w kodzie aplikacji.

2767 słów

Końce punktowe CRUD przede wszystkim powtarzają informacje już zawarte w schemacie Prisma. Nazwa modelu staje się segmentem ścieżki, a pola skalarne przekształcają się w mechanizmy walidacji danych wejściowych. Wywołania Prisma stają się metodami kontrolera, a relacje oznaczają kolejną warstwę przetwarzania przychodzących żądań oraz kształtowania tego, co jest zwracane.

To spojrzenie na ten temat opiera się na narzędziu otwartego oprogramowania stworzonym przez jego utrzymującego, ocenianym na podstawie aktualnej dokumentacji oraz eksperymentów, które można samodzielnie powtórzyć, a nie na jakichkolwiek twierdzeniach dotyczących jego powszechnego użycia.

To powtarzanie jest kosztowne właśnie dlatego, że wydaje się nieszkodliwe. Każdy ręcznie napisany obsługiwacz, który kopiujemy, to kolejne miejsce, gdzie domyślne ustawienia paginacji, dozwolone pola, zakres dostępu użytkowników oraz obsługa błędów mogą potajemnie odbiegać od pozostałych.

prisma-generator-express odbiera to żmudne zadanie z twoich rąk i włącza je do kroku prisma generate. Może tworzyć routery dla Express, Fastify lub Hono. Pakiet towarzyszący, prisma-guard, generuje jednocześnie metadane walidacji i zakresu dostępu uwzględniające Prisma, a specyfikacje operacji precyzyjnie określają, jakie argumenty może wysłać każdy typ wywołującego.

W rezultacie nie otrzymujemy aplikacji bez kodu. To aplikacja z znacznie mniej elementów obsługujących warstwę graniczną oraz z o wiele jaśniejszą strukturą decyzyjną w pozostałych kwestiach.

To rozróżnienie jest kluczowe. Proces generowania powinien przejąć wszystko to, co sam schemat może w pełni opisać. Autoryzacja, operacje, które są dostępne, tożsamość wywołujących oraz wszelkie zasady wymagające wiedzy wykraczającej poza schemat muszą nadal znajdować się w kodzie aplikacji.

Zmień powtarzalną pracę na jeden krok generowania

Generowana API zaczyna się od trzech powiązanych elementów: Prisma Client, metadanych ochronnych oraz samych routerów HTTP.

generator client {
  provider = "prisma-client-js"
}
generator guard {
  provider          = "prisma-guard"
  output            = "../generated/guard"
  enforceProjection = "true"
}generator express {
  provider = "prisma-generator-express"
  target   = "express"
}

Wykonanie polecenia npx prisma generate raz ponownie generuje wszystkie trzy pliki w momencie zmiany schematu lub konfiguracji generatora.

Schemat pozostaje jedynym źródłem prawdy dla modelu danych. Generowane pliki routerów to wyłącznie wynik procesu budowania, nic więcej. Konfiguracja routerów określa, które operacje zostaną faktycznie włączone, a metadane ochronne określają, jakie argumenty Prisma może używać dany użytkownik.

Rozdzielenie tych kwestii jest o wiele bardziej przydatne niż traktowanie kodu generowanego jako zamiennika architektury. Celowo do całego procesu wprowadzane są dwa odrębne elementy wejściowe: generowanie oparte na schemacie zajmuje się powtarzalnymi mechanizmami, natomiast zasady na poziomie aplikacji regulują decyzje dotyczące zaufania oraz dostępu do tras.

Jeśli jakąś zasadę nie można wiernie wyrazić za pomocą generatora lub specjalnego formatu ochronnego, nie próbujcie ją wpisać do konfiguracji. Specjalny mechanizm obsługi lub zasada egzekwowana w bazie danych stanowią czystsze rozwiązanie niż deklaratywna ustawienie, które w tajemnicy kłamie na temat swoich funkcji.

Proces generowania zmienia również to, co faktycznie jest sprawdzane podczas przeglądu kodu. Ręcznie napisany kod CRUD skłania recenzentów do sprawdzania na każdej linii powtarzającej się logiki parsowania i delegacji. Kod CRUD wygenerowany automatycznie skupia tę kontrolę na znacznie mniejszej liczbie elementów: schemacie Prisma, opcjach generatora, deskrypcjach tras, strukturach oraz tym, co w rozwiązaniu ustala zaufany kontekst.

To nie oznacza, że wygenerowany wynik jest nieważny — po prostu oznacza to, że bezpośrednia edycja tego wyniku to niewłaściwy sposób postępowania. Jeśli trasa wymaga poprawek, należy zmienić konfigurację, która ją tworzy, i ponownie wygenerować kod. Ręczna korekta wplątana do pliku routera może zniknąć przy następnej zmianie schematu i nie pozostawi żadnego śladu tego, jaki kontrakt był pierwotnie zamierzony.

Wersjonowanie wymaga tej samej dyscypliny. Ustal konkretne wersje pakietu Pin Prisma, pakietu ochronnego oraz generatora routera, zregeneruj kod na czystym kontekście projektu i przeprowadź testy kontraktów wobec uzyskanego wyniku. Kod wygenerowany nadal stanowi część twojej struktury zależności, nawet jeśli repozytorium nie traktuje każdej wygenerowanej linii jako kodu napisanego ręcznie.

Prawdziwa korzyść szybkościowa wynika z możliwości powtarzalności. Jedna edycja schematu może jednocześnie odświeżyć metadane walidacji, typy klienta oraz mechanizmy routera. Dzięki temu przegląd skupia się wyłącznie na stosunkowo wąskiej warstwie zasad — tej części, która rzeczywiście nie może zostać wywnioskowana wyłącznie na podstawie modelu.

Niech jeden model służy kilku celowym kontraktom

Jeden model Prisma może jednocześnie obsługiwać kilka różnych interfejsów skierowanych do klienta.

Na przykład rekord pokoju hotelowego może pojawić się na stronie publicznego wyszukiwania, w strumieniu danych partnera oraz w wewnętrznej konsoli personelu. Ci trzej użytkownicy nie powinni być zmuszani do korzystania z jednego rozbudowanego zestawu wszystkich pól i operacji, których mogliby potrzebować.

Nazwane kształty umożliwiają jednej generowanej operacji obsługę kilku odrębnych umów jednocześnie:

const roomRoutes = {
  findMany: {
    shape: {
      storefront: {
        where: {
          isPublished: { equals: force(true) },
          name: { contains: true },
        },
        select: { id: true, name: true, nightlyRate: true },
        take: { max: 40, default: 20 },
      },
      backoffice: {
        where: {
          name: { contains: true },
          floor: { equals: true },
        },
        select: {
          id: true,
          name: true,
          nightlyRate: true,
          floor: true,
          internalNote: true,
        },
        take: { max: 200, default: 50 },
      },
    },
  },
}

Każda nazwana klucz definiuje kompletną, samodzielną umowę API. Użytkownik publiczny nie może rozszerzyć swojej projekcji o pole internalNote, ponieważ to pole po prostu nie istnieje w publicznym kształcie. Personel może uzyskać znacznie bogatszą projekcję, nie zmuszając pozostałych klientów do korzystania z ich własnych, ręcznie napisanych mechanizmów.

Użyj shape, gdy tylko umowa na poziomie Prisma musi się różnić pomiędzy użytkownikami. Użyj variants, gdy dany użytkownik potrzebuje również swoich własnych, dedykowanych hooków.

Sposób identyfikacji osoby dzwoniącej stanowi sam w sobie element zasobów bezpieczeństwa. Nagłówek żądania jest traktowany jako dane dostarczone przez klienta — nadaje się do celowych, publicznych rozróżnień, takich jak widok zwięzły w porównaniu z szczegółowym, ale nie powinien służyć do wyboru uprzywilejowanego kontraktu pracowniczego.

Dla wszystkich uprzywilejowanych przypadków należy zamiast tego identyfikować osobę dzwoniącą za pomocą funkcji resolveVariant, wykorzystując uwierzytelnione dane po stronie serwera. Najpierw sprawdza się dokładne klucze identyfikujące dzwoniącą osobę, a dopiero potem te parametryzowane. Klucz default obsługuje przypadki braku klucza, pustych wartości lub niepasujących identyfikatorów, więc należy go zdefiniować tylko wtedy, gdy jesteś pewien, że wszystkie te trzy sytuacje będą kierowane do tego rozwiązania awaryjnego.

Czasami lepiej całkowicie pominąć dany kontrakt niż dodawać kolejną kontrolę autoryzacji. Jeśli partnerzy nie powinni mieć możliwości usuwania pomieszczeń, po prostu nie przypisuj generowanej operacji usuwania żadnego klucza partnera.

Pomaga przyjrzenie się routingu połączeń w formie siatki: operacje wzdłuż jednej osi, grupy odbiorców wzdłuż drugiej. Każda komórka powinna zawierać kształt z odpowiednio umieszczonymi elementami lub być celowo pusta.

Zachowuj te kontrakty jako odrębne nazwy, nawet jeśli znacznie się one pokrywają pod względem pól. Dzielenie się obiektem jest dość bezpieczne w ramach jednego poziomu zaufania, ale ponowne użycie tego samego obiektu przez publiczne i uprzywilejowane grupy odbiorców niesie ryzyko cichego rozszerzania obu końcówek w momencie dodania nowego pola. Niewielka redundancja na granicy poziomów zaufania często jest warta zachodu, ponieważ znacznie ułatwia zrozumienie, kto faktycznie otrzymuje jakie dane.

Parametryzowane klucze wywołujące dają ci jeszcze jeden powód, by polegać na wbudowanym rozwiązaniu zamiast ponownie projektować mechanizm wyboru wywołującego wewnątrz hooka. Router trzyma surową wartość wywołującego oddzielnie od deklarowanego klucza, z którym ją porównuje, i odrzuca bezwzględnie niejednoznaczne wzory parametrów. Ręczne porównanie ciągów znaków musiałoby odtworzyć dokładne dopasowanie, hierarchię parametrów, obsługę domyślną oraz zachowanie w przypadku błędów, zanim mogłoby twierdzić, że oferuje te same gwarancje.

Używaj hooków do podejmowania decyzji związanych z cyklem życia aplikacji, a nie do tworzenia ukrytych zapytań

Szablony tras generowane nie eliminują potrzeby podjęcia decyzji na poziomie aplikacji. Po prostu zapewniają tym decyzjom przewidywalne miejsce realizacji.

Dla żądania, które pasuje do określonej wersji, przepływ wykonywania przebiega przez przedhooki na poziomie operacji, następnie przedhooki na poziomie wersji, potem sam wygenerowany obsługiwacz, dalej poślizgowe hooki na poziomie wersji i wreszcie poślizgowe hooki na poziomie operacji.

Hooki operacyjne to odpowiednie miejsce na zasady dotyczące każdego wywołującego tę operację, niezależnie od tego, do jakiej umowy pasuje. Hooki wersji należą do logiki specyficznej dla konkretnej deklarowanej struktury wywołującego.

const transferRoutes = {
  update: {
    before: [authenticateOperator],
    variants: {
      warehouse: {
        before: [authorizeTransferLocation],
        shape: warehouseTransferShape,
      },
      supervisor: {
        before: [requireSupervisorApproval],
        shape: supervisorTransferShape,
      },
    },
  },
}

Przedhook może swobodnie sprawdzić dokładny identyfikator, który ma zostać użyty przez wygenerowany obsługiwacz, i może odrzucić żądanie bezpośrednio, jeśli ten identyfikator nie przejdzie sprawdzenia. Nigdy nie powinien on zatwierdzić jednego identyfikatora, a jednocześnie potajemnie zastąpić go innym w rzeczywistej zapytaniu. Taka cicha modyfikacja unieważnia cel posiadania obsługiwacza, który można sprawdzić.

Ograniczenia, które nigdy się nie zmieniają, powinny znajdować się w kształtach. Filtrowanie na poziomie najemcy, które jest stosowane na początku zapytania, powinno być umieszczone w generowanych mapach zakresu w połączeniu z zaufanym kontekstem. Różnice pomiędzy typami wywołujących powinny być uwzględniane w wariantach. Każdy z tych elementów ma przypisane konkretne miejsce, a ich pomieszanie powoduje, że logika zostaje ukryta w miejscu, gdzie nikt jej nie szuka.

Istnieją przypadki, gdy serwer rzeczywiście musi stworzyć zapytanie, którego nie może wyrazić żaden kształt. Wtedy przydaje się specjalnie zaprojektowany obsługujący element – szczególnie gdy potrzebna jest prawdziwa disjunkcja należąca do serwera. Warto pamiętać, że warunki wymuszone, umieszczone wewnątrz kombinatorów logicznych, stają się obowiązkowymi ograniczeniami dla zapytania, a nie elastycznym mechanizmem do wyrażania dowolnych reguł autoryzacji. Traktowanie ich jako uniwersalnego silnika logicznego to częsty sposób na powstanie reguł, które w rzeczywistości nie realizują tego, co się sądzi, że realizują.

After-hooks są wykonywane po obsłudze zdarzenia, ale nie stanowią fazy czyszczenia, na której można bezwarunkowo polegać. Odpowiedź zakończona przed czasem lub błąd wystąpiły w trakcie żądania mogą uniemożliwić wykonanie późniejszych faz – włączając after-hooks. Jeśli zasób musi zostać zwolniony bez względu na wszystko, potrzebuje własnego cyklu życia z wyraźnym blokiem finally umieszczonym poza generowanym łańcuchem hooków, a nie w jego obrębie.

Konkretne zasady zależą również od wybranego frameworka. Express, Fastify i Hono implementują sygnatury hooków oraz mechanizmy przerywania w różny sposób. Ogólna zasada – to, do której części kodu należy dana decyzja – pozostaje taka sama we wszystkich trzech, ale sam kod aplikacji musi być zgodny z kontraktem danego frameworka.

Zachowaj zaufany kontekst poza argumentami Prisma

Tożsamość najemcy oraz stan autoryzowanego dzwoniącego nigdy nie powinny trafiać na serwer jako pola kontrolowane przez klienta w ciele zapytania.

Zamiast tego należy zastosować marker @scope-root do modelu najemcy, uruchomić proces generowania w celu stworzenia odpowiadającej mapy zakresów oraz przymocować rozwiązanie kontekstowe do Prisma Client za pośrednictwem jego mechanizmu rozszerzeń:

const prisma = new PrismaClient().$extends(
  guard.extension(() => ({
    Nursery: requestStore.getStore()?.nurseryId,
  }))
)

Sama wartość pochodzi ze stanu autoryzowanego, lokalnego wobec żądania — a nie z niczego, co wysyła klient. Następnie rozszerzenie wstrzykuje ją do obsługiwanych operacji najwyższego poziomu na modelach mapowanych jako dzieci tego korzenia zakresu.

To jest rzeczywista, dobrze zdefiniowana funkcja, a nie ogólna gwarancja, że każda relacja zostanie automatycznie zablokowana. Wymuszanie zakresu nie obejmuje zagnieżdżonych operacji odczytu ani zapisu. Sam model delegata korzeniowego nie jest filtrowany przez swój własny marker zakresu. Każdy model, który nie ma wygenerowanego mapowania, nadal wymaga własnej, wyraźnej ochrony – kontekst zakresu nie obejmie go domyślnie.

Szybkość generowania jest wciąż wartościowa właśnie dlatego, że te granice są widoczne, a nie ukryte. Można bezpośrednio przejrzeć mapę zakresu. Zagnieżdżone projekcje mogą mieć własne, niezależne filtry i ograniczenia. Każda nietypowa zasada własności, która nie pasuje do standardowego wzorca, może zostać przeniesiona do kodu aplikacji lub obsłużona na poziomie bazy danych.

Stan aplikacji dostosowanej – wszystko, co wykracza poza zakres użytkownika – powinien znajdować się w kontekście żądania, a nie w samych argumentach Prisma. Wtajemniczanie identyfikatora wywołującego lub metadanych autoryzacji do ciała żądania Prisma sprawia, że wynikający kontrakt danych staje się znacznie trudniejszy do zrozumienia, a także może spowodować błędy w rygorystycznej walidacji, która oczekuje czystej struktury argumentów.

Traktuj dostępność tras jako element projektu produktu

Generator jest w stanie tworzyć obsługi dla dużej liczby operacji Prisma. Ta zdolność nic jednak nie mówi o tym, które z tych obsług powinny faktycznie być dostępne i podłączone.

Czytanie, mutacje pojedynczych rekordów, mutacje masowe, zapisy relacji oraz operacje zwracające dane wymagają osobnej oceny, a nie pojedynczej decyzji ogólnej. Wsparcie dostawcy dla niektórych operacji masowych zwracających dane jest zróżnicowane, więc nie chodzi tu tylko o kwestie polityki — to także problem kompatybilności. Każda ścieżka, która nie ma atrybutów shape i variants, będzie bezpośrednio wywoływać Prisma bez żadnych mechanizmów kontroli.

Dobrze przemyślana konfiguracja nie powstaje poprzez włączenie wszystkiego, a następnie dodanie kontrol ograniczeń później. Zaczyna się od małej, jasno określonej bazy i rozwija się tylko wtedy, gdy rzeczywisty proces pracy produktu pokazuje potrzebę dodatkowej operacji.

Czytanie projektacji wymaga takiego samego poziomu uwagi jak dostęp do zapisu. W przypadku chronionego odczytu, select lub include zadeklarowane na poziomie kształtu pełnią rolę zarówno listy białych, jak i domyślnej wartości, gdy żądanie klienta nie zawiera własnej projektacji. Projektacja mutacji podlega innym zasadom domyślnym, a jeśli pominięcie projektacji nie powinno nigdy prowadzić do powiększenia odpowiedzi, konieczne jest wyraźne włączenie opcji enforceProjection.

Trasy masowe wymagają innego podejścia za każdym razem. Metoda masowa powinna być uznawana za ważną w wygenerowanej strukturze tylko wtedy, gdy jej forma określa odpowiedni zasób do filtrowania, a przychodząca prośba dostarcza nadal sensownych warunków w czasie wykonywania. Włączanie funkcji deleteMany tylko dlatego, że możliwe jest usuwanie pojedynczych rekordów, pomija ten drugi, odrębny ryzyko. Zwracanie wariantów operacji masowych niesie ze sobą dodatkową zależność od dostawcy i wsparcia Prismy, dlatego konfiguracja tras powinna odzwierciedlać to, co faktycznie może wykonać zainstalowana baza danych, a nie to, co plan rozwoju produktu sugerowałby jako optymalne.

Wygenerowany plik OpenAPI może opisywać ścieżki tras oraz strukturę żądania uzyskaną na podstawie określonych wzorów. Nie może zaglądać do dowolnych funkcji typu hook, więc nie jest w stanie opisać zasad ukrytych w ich wnętrzu. Jeśli hook blokuje transakcje, które wykraczają poza wyznaczony magazyn operatora, ta warunek musi zostać udokumentowany obok konfiguracji trasy i zweryfikowany za pomocą testów sprawdzających zachowanie aplikacji — wygenerowaną dokumentację nigdy nie należy mylić z dowodem na logikę, której nie ma możliwości sprawdzenia.

Wersje GET i POST punktu końcowego służącego do odczytu powinny mieć wspólny kontrakt zapytania. Metoda GET polega na używaniu zakodowanych parametrów zapytania, natomiast POST przyjmuje surowy format JSON, który jest bardziej praktyczny w przypadku złożonych struktur argumentów. Hook, który dotyka jedynie ciała żądania, powoduje zachowanie zależne od metody transmisji, i właśnie dlatego nie powinno się tam umieszczać restrykcji stabilności.

Przyjmij generowanie bez rezygnacji z recenzji

Pрактиczny sposób oceny tego typu konfiguracji polega na wykonaniu krótkiej sekwencji kroków:

  1. Stwórz router dla pojedynczego modelu tylko do odczytu.
  2. Ujawnij jedynie te operacje, które są rzeczywiście potrzebne.
  3. Dodaj jeden bezpośredni kształt z wyraźną projekcją oraz ograniczeniem wielkości strony.
  4. Dodaj zaufany kontekst zakresu, jeśli model jest mapowany do dzierżawy.
  5. Rozdziel operację na oddzielne umowy dla wywołujących tylko wtedy, gdy grupy użytkowników rzeczywiście się różnią.
  6. Dodaj hooki wyłącznie dla decyzji, których kształty, zakresy i warianty same nie mogą podjąć.
  7. Wprowadź operacje zapisu dopiero po tym, jak istnieją wyraźne testy obejmujące kompletność tworzenia, filtrowanie masowe oraz własność relacji.

Należy zachować testy kontraktów typu real-guard nawet w środowiskach, gdzie testy end-to-end przeglądarki działają przy konfiguracji, która całkowicie pomija weryfikację zasad bezpieczeństwa. Testy przeglądarki dobrze sprawdzają się przy obsłudze routingu i zachowań interfejsu, ale nie mogą udowodnić, że wersja produkcyjna odrzuci nieautoryzowane pola, gdy warstwa zabezpieczeń faktycznie nie istnieje.

Generatory przydają się, ponieważ pozwalają zespołowi skupić się na decyzjach, które naprawdę mają znaczenie. Prisma opisuje dane, generatory zajmują się powtarzalną pracą mechaniczną, a struktury określają, które wywołania są dozwolone. Kod aplikacji ma za zadanie zapewnić zaufanie, politykę specyficzną dla produktu oraz wyjątki, których nie można uczciwie opisać w inny sposób.

Literatura pokrewna

  • Raw SQL, Prisma czy Drizzle: Jak faktycznie wybrać warstwę bazy danych — Dowiedz się, w jaki sposób raw SQL, Prisma i Drizzle różnią się pod względem kontroli, bezpieczeństwa typów i doświadczenia programisty, oraz poznaj praktyczny sposób wyboru odpowiedniego rozwiązania dla danego projektu.
  • Przewodnik po MovieVault: API listy oglądania z Express 5, Prisma 7 i JWT — Specyfikacja ćwiczenia full-stack z wyznaczonym czasem realizacji oraz jego backend oparty na Express, Prisma i JWT, wraz z notatkami dotyczącymi sprawdzania uprawnień, mechanizmów kaskadowych i obsługi błędów.
  • Przegląd prisma-guard Shapes: Własność, Projekcja i Kontrakty Zapisu — Naucz się analizować prisma-guard shapes jako kontrakty API, pytając o to, kto posiada każdą wartość, jakie dane mogą zostać wysłane w odpowiedzi oraz które operacje zapisu może wykonać utworzony endpoint.
  • Wykrywanie driftu kontraktów API w czasie kompilacji za pomocą stopniowego wdrażania tRPC — Jak tRPC przekształca przeimkowane pole backendu w błąd kompilacji, jak wprowadzać je endpoint po endpoint obok REST oraz w których przypadkach jest niewłaściwym narzędziem.