Jakie są najlepsze praktyki dotyczące zagnieżdżonych zasobów REST?


301

O ile mogę stwierdzić, każdy pojedynczy zasób powinien mieć tylko jedną ścieżkę kanoniczną . Więc w poniższym przykładzie, jakie byłyby dobre wzorce adresów URL?

Weźmy na przykład reprezentację firm. W tym hipotetycznym przykładzie każda firma posiada 0 lub więcej działów, a każdy dział posiada 0 lub więcej pracowników.

Dział nie może istnieć bez powiązanej firmy.

Pracownik nie może istnieć bez powiązanego działu.

Teraz znajdę naturalną reprezentację wzorców zasobów.

  • /companies Zbiór firm - Akceptuje dla nowej firmy. Zdobądź całą kolekcję.
  • /companies/{companyId}Indywidualna firma. Akceptuje GET, PUT i DELETE
  • /companies/{companyId}/departmentsAkceptuje POST dla nowego przedmiotu. (Tworzy dział w firmie).
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Biorąc pod uwagę ograniczenia, w każdej sekcji uważam, że ma to sens, jeśli jest nieco zagnieżdżone.

Jednak moja trudność pojawia się, gdy chcę wymienić ( GET) wszystkich pracowników we wszystkich firmach.

Wzorzec zasobów dla tego byłby najbardziej zbliżony do /employees(Zbiór wszystkich pracowników)

Czy to oznacza, że ​​powinienem /employees/{empId}również mieć, ponieważ jeśli tak, to istnieją dwa identyfikatory URI, aby uzyskać ten sam zasób?

A może cały schemat powinien zostać spłaszczony, ale oznaczałoby to, że pracownicy są zagnieżdżonymi obiektami najwyższego poziomu.

Na poziomie podstawowym /employees/?company={companyId}&department={deptId}zwraca dokładnie ten sam widok pracowników, co najgłębiej zagnieżdżony wzór.

Jaka jest najlepsza praktyka w zakresie wzorców adresów URL, w których zasoby są własnością innych zasobów, ale powinny być dostępne osobne zapytania?


1
Jest to prawie dokładnie problem przeciwny do opisanego w stackoverflow.com/questions/7104578/…, chociaż odpowiedzi mogą być powiązane. Oba pytania dotyczą własności, ale ten przykład sugeruje, że obiekt najwyższego poziomu nie jest właścicielem.
Wes

1
Dokładnie to, nad czym się zastanawiałem. Dla danego przypadku użycia twoje rozwiązanie wydaje się w porządku, ale co jeśli relacja jest raczej agregacją niż kompozycją? Wciąż próbujesz dowiedzieć się, jaka jest tutaj najlepsza praktyka ... Czy to rozwiązanie implikuje jedynie stworzenie relacji, np. Zatrudniona jest istniejąca osoba, czy też tworzy obiekt osoby?
Jakob O.

Tworzy osobę na moim fikcyjnym przykładzie. Powodem, dla którego użyłem tych terminów dotyczących domeny, jest rozsądnie zrozumiały przykład, chociaż naśladuję mój rzeczywisty problem. Czy przejrzałeś powiązane pytanie, które może jeszcze bardziej Cię powstrzymać w związku z agregacją.
Wes

Podzieliłem moje pytanie na odpowiedź i pytanie.
Wes

Odpowiedzi:


152

To, co zrobiłeś, jest poprawne. Zasadniczo do tego samego zasobu może być wiele identyfikatorów URI - nie ma reguł, które mówią, że nie powinieneś tego robić.

Ogólnie rzecz biorąc, być może będziesz musiał uzyskać dostęp do elementów bezpośrednio lub jako podzbiór czegoś innego - więc twoja struktura ma dla mnie sens.

Tylko dlatego, że pracownicy są dostępni w dziale:

company/{companyid}/department/{departmentid}/employees

Nie oznacza to, że nie mogą być również dostępne w firmie:

company/{companyid}/employees

Co zwróci pracowników dla tej firmy. Zależy to od tego, czego potrzebuje Twój konsumujący klient - do tego właśnie powinieneś projektować.

Mam jednak nadzieję, że wszystkie procedury obsługi adresów URL używają tego samego kodu zabezpieczającego do zaspokojenia żądań, aby nie powielać kodu.


11
Wskazuje to na duch RESTful, nie ma zasad, które mówią, że powinieneś lub nie powinieneś, jeśli tylko najpierw rozważasz znaczący zasób . Ale zastanawiam się, jaka jest najlepsza praktyka, aby nie powielać kodu w takich scenariuszach.
abookyun

13
@abookyun, jeśli potrzebujesz obu tras, to powtarzający się kod kontrolera między nimi można wyodrębnić w celu obsługi obiektów.
bgcode

To nie ma nic wspólnego z REST. REST nie dba o to, jak zorganizować część ścieżki adresów URL ... wszystko to obchodzi jest ważna, miejmy nadzieję trwałe URI ...
redben

Kierując się tą odpowiedzią, uważam, że każdy interfejs API, w którym wszystkie segmenty dynamiczne są unikalnymi identyfikatorami, nie powinien obsługiwać wielu segmentów dynamicznych ( /company/3/department/2/employees/1). Jeśli interfejs API zapewnia sposoby uzyskania każdego zasobu, wówczas każde z tych żądań można wykonać w bibliotece po stronie klienta lub jako jednorazowy punkt końcowy, który ponownie wykorzystuje kod.
maks.

1
Chociaż nie ma zakazu, uważam, że bardziej elegancko jest mieć tylko jedną ścieżkę do zasobu - upraszcza wszystkie modele mentalne. Wolę również, aby identyfikatory URI nie zmieniały typu zasobu, jeśli występuje zagnieżdżenie. na przykład /company/*powinien zwracać tylko zasób firmy i w ogóle nie zmieniać typu zasobu. Żadna z tych informacji nie jest określona przez REST - jest to ogólnie słabo określona - tylko osobiste preferencje.
kashif

174

Wypróbowałem obie strategie projektowania - zagnieżdżone i nie zagnieżdżone punkty końcowe. Znalazłem to:

  1. jeśli zagnieżdżony zasób ma klucz podstawowy, a nie masz jego nadrzędnego klucza podstawowego, struktura zagnieżdżona wymaga jego uzyskania, nawet jeśli system go nie wymaga.

  2. zagnieżdżone punkty końcowe zwykle wymagają redundantnych punktów końcowych. Innymi słowy, będziesz potrzebować dodatkowego punktu końcowego / pracowników, aby uzyskać listę pracowników z różnych działów. Jeśli masz / pracowników, co dokładnie kupują / firmy / działy / pracownicy?

  3. zagnieżdżanie punktów końcowych nie rozwija się tak ładnie. Np. Być może nie będziesz musiał szukać pracowników teraz, ale możesz później, a jeśli masz zagnieżdżoną strukturę, nie masz innego wyboru, jak dodać kolejny punkt końcowy. W przypadku projektu zagnieżdżonego wystarczy dodać więcej parametrów, co jest prostsze.

  4. czasami zasób może mieć wiele rodzajów rodziców. W rezultacie powstaje wiele punktów końcowych, wszystkie zwracają ten sam zasób.

  5. redundantne punkty końcowe utrudniają pisanie dokumentów, a także utrudniają naukę interfejsu API.

Krótko mówiąc, wydaje się, że nie zagnieżdżony projekt pozwala na bardziej elastyczny i prostszy schemat punktu końcowego.


24
Odświeżenie tej odpowiedzi było bardzo odświeżające. Używam zagnieżdżonych punktów końcowych od kilku miesięcy po tym, jak nauczono mnie, że to „właściwa droga”. Doszedłem do tych samych wniosków, które wymieniliście powyżej. O wiele łatwiej dzięki nie zagnieżdżonemu projektowi.
user3344977

6
Wygląda na to, że podajesz niektóre wady jako wady. „Po prostu wciśnij więcej parametrów w jeden punkt końcowy” sprawia, że ​​interfejs API jest trudniejszy do udokumentowania i nauki, a nie na odwrót. ;-)
Drenmi

4
Nie jestem fanem tej odpowiedzi. Nie ma potrzeby wprowadzania redundantnych punktów końcowych tylko dlatego, że dodałeś zagnieżdżony zasób. Nie jest również problemem, aby ten sam zasób został zwrócony przez wielu rodziców, pod warunkiem, że ci rodzice rzeczywiście posiadają zagnieżdżony zasób. Zdobycie zasobu nadrzędnego do nauki interakcji z zagnieżdżonymi zasobami nie stanowi problemu. Powinien to zrobić dobry wykrywalny interfejs API REST.
Scottm,

3
@Scottm - Jedną z wad zagnieżdżonych zasobów, z którymi się zetknąłem, jest to, że może prowadzić do zwracania niepoprawnych danych, jeśli identyfikatory zasobów nadrzędnych są niepoprawne / niezgodne. Zakładając, że nie ma problemów z autoryzacją, do implementacji interfejsu API należy sprawdzenie, czy zagnieżdżony zasób rzeczywiście jest potomkiem przekazywanego zasobu nadrzędnego. Jeśli ta kontrola nie jest zakodowana, odpowiedź interfejsu API może być niepoprawna, co może prowadzić do uszkodzenia. Jakie są Twoje myśli?
Andy Dufresne,

1
Nie potrzebujesz pośrednich identyfikatorów nadrzędnych, jeśli wszystkie zasoby końcowe mają unikalne identyfikatory. Na przykład, aby uzyskać pracownika według identyfikatora, masz GET / firmy / działy / pracownicy / {empId} lub aby dostać wszystkich pracowników w firmie 123, masz GET / firmy / 123 / działy / pracownicy / Utrzymanie hierarchicznej ścieżki sprawia, że ​​bardziej widoczne jest możesz dostać się do zasobów pośrednich w celu filtrowania / tworzenia / modyfikowania i moim zdaniem pomaga w wykrywalności.
PaulG

77

Przesunąłem to, co zrobiłem, z pytania do odpowiedzi, w której prawdopodobnie więcej osób je zobaczy.

To, co zrobiłem, to mieć punkty końcowe tworzenia w zagnieżdżonym punkcie końcowym. Kanoniczny punkt końcowy do modyfikowania lub odpytywania elementu nie znajduje się w zagnieżdżonym zasobie .

Tak więc w tym przykładzie (wystarczy wymienić punkty końcowe, które zmieniają zasób)

  • POST /companies/ tworzy nową firmę zwraca link do utworzonej firmy.
  • POST /companies/{companyId}/departments po umieszczeniu działu tworzy nowy dział zwraca link do /departments/{departmentId}
  • PUT /departments/{departmentId} modyfikuje dział
  • POST /departments/{deparmentId}/employees tworzy nowy pracownik zwraca link do /employees/{employeeId}

Istnieją więc zasoby na poziomie głównym dla każdej kolekcji. Jednak tworzenie odbywa się w obiekcie będącym właścicielem .


4
Wymyśliłem również ten sam typ projektu. Wydaje mi się, że intuicyjne jest tworzenie takich rzeczy „tam, gdzie należą”, ale nadal można je wymienić na całym świecie. Tym bardziej, gdy istnieje związek, w którym zasób MUSI mieć rodzica. Następnie globalne utworzenie tego zasobu nie czyni tego oczywistym, ale robienie tego w takim pod-zasobie ma sens.
Joakim,

Wydaje mi się, że użyłeś POSTznaczenia PUT, i inaczej.
Gerardo Lima,

Właściwie nie zauważam, że nie używam wstępnie przypisanych identyfikatorów do tworzenia, ponieważ serwer w tym przypadku jest odpowiedzialny za zwrócenie identyfikatora (w linku). Dlatego pisanie testu POST jest poprawne (nie można uzyskać tej samej implementacji). Put zmienia jednak cały zasób, ale nadal jest dostępny w tej samej lokalizacji, więc umieszczam go. PUT vs POST to inna sprawa i jest również kontrowersyjna. Na przykład stackoverflow.com/questions/630453/put-vs-post-in-rest
Wes

@Wes Even Wolę modyfikować metody czasowników, aby były nadrzędne. Ale czy widzisz, że przekazywanie parametru zapytania dla zasobu globalnego jest dobrze akceptowane? Np .: POST / działy z parametrem zapytania company = id firmy
Ayyappa

1
@Mohamad Jeśli uważasz, że inny sposób jest łatwiejszy zarówno w zrozumieniu, jak i stosowaniu ograniczeń, możesz dać odpowiedź. W tym przypadku chodzi o wyraźne mapowanie. Może działać z parametrem, ale tak naprawdę to jest pytanie. Jaka jest najlepsza droga.
Wes

35

Przeczytałem wszystkie powyższe odpowiedzi, ale wygląda na to, że nie mają wspólnej strategii. Znalazłem dobry artykuł na temat najlepszych praktyk w API Design z Microsoft Documents . Myślę, że powinieneś polecić.

W bardziej złożonych systemach kuszące może być dostarczanie identyfikatorów URI, które umożliwiają klientowi nawigację na kilku poziomach relacji, takich jak: /customers/1/orders/99/products.Jednak ten poziom złożoności może być trudny do utrzymania i nieelastyczny, jeśli relacje między zasobami zmienią się w przyszłości. Zamiast tego staraj się, aby identyfikatory URI były stosunkowo proste . Gdy aplikacja ma odwołanie do zasobu, powinno być możliwe użycie tego odwołania do znalezienia elementów związanych z tym zasobem. Poprzednie zapytanie można zastąpić identyfikatorem URI, /customers/1/ordersaby znaleźć wszystkie zamówienia dla klienta 1, a następnie /orders/99/productsznaleźć produkty w tym zamówieniu.

.

Wskazówka

Unikaj wymagania, aby identyfikatory URI zasobów były bardziej złożone niż collection/item/collection.


3
Odniesienia, które podajesz, są niesamowite, a wyróżnia Cię to, że nie tworzysz skomplikowanych identyfikatorów URI.
vicco

Więc jeśli chcę utworzyć zespół dla użytkownika, powinien to być POST / drużyn (userId w tobie) lub POST / users /: id / drużyn
coinhndp

@coinhndp Cześć, Powinieneś użyć POST / drużyn i możesz dostać userId po autoryzacji tokena dostępu. Mam na myśli, że kiedy tworzysz coś, potrzebujesz kodu autoryzacyjnego, prawda? Nie wiem, jakich ram używasz, ale jestem pewien, że możesz uzyskać identyfikator użytkownika w kontrolerze API. Na przykład: W interfejsie API ASP.NET wywołaj RequestContext.Principal z poziomu metody ApiController. W Spring Secirity pomoże Ci SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal (). W AWS NodeJS Lambda, czyli cognito: nazwa użytkownika w obiekcie nagłówków.
Long Nguyen,

Więc co jest nie tak z POST / users /: id / zespołów. Myślę, że jest to zalecane w opublikowanym powyżej dokumencie Microsoft
coinhndp

@coinhndp Jeśli tworzysz zespół jako administrator, to dobrze. Ale, jak zwykli użytkownicy, nie wiem, dlaczego potrzebujesz ID użytkownika na ścieżce? Przypuszczam, że mamy user_A i user_B, co myślisz, jeśli użytkownik_A mógłby stworzyć nowy zespół dla użytkownika_B, jeśli użytkownik_A wywoła POST / users / user_B / drużyny. Zatem w tym przypadku nie trzeba przekazywać identyfikatora użytkownika, identyfikator użytkownika może uzyskać po autoryzacji. Ale zespoły /: id / projects dobrze jest na przykład utworzyć relację między zespołem a projektem.
Long Nguyen,

10

Wygląd adresów URL nie ma nic wspólnego z usługą REST. Wszystko idzie. W rzeczywistości jest to „szczegół implementacji”. Więc tak jak nazywasz swoje zmienne. Muszą być jedyne w swoim rodzaju i trwałe.

Nie marnuj na to zbyt wiele czasu, po prostu dokonaj wyboru i trzymaj się go / bądź konsekwentny. Na przykład, jeśli korzystasz z hierarchii, robisz to dla wszystkich swoich zasobów. Jeśli korzystasz z parametrów zapytań ... itd., Podobnie jak konwencje nazewnictwa w kodzie.

Dlaczego tak ? O ile mi wiadomo, API „RESTful” ma być możliwe do przeglądania (wiesz… „Hypermedia jako silnik stanu aplikacji”), dlatego klient API nie dba o to, jakie są twoje adresy URL, o ile są ważne (nie ma SEO, żaden człowiek nie musiałby czytać tych „przyjaznych adresów URL”, z wyjątkiem tego, że może służyć do debugowania ...)

To, jak ładny / zrozumiały jest adres URL w interfejsie API REST, jest interesujące tylko dla programisty interfejsu API, a nie dla klienta interfejsu API, tak jak nazwa zmiennej w kodzie.

Najważniejsze jest to, że klient interfejsu API wie, jak interpretować typ multimediów. Na przykład wie, że:

  • Twój typ multimediów ma właściwość links, która zawiera listę dostępnych / powiązanych linków.
  • Każdy link jest identyfikowany przez relację (tak jak przeglądarki wiedzą, że link [rel = "arkusz stylów"] oznacza, że ​​jego arkusz stylów lub rel = favico jest linkiem do favicon ...)
  • i wie, co oznaczają te relacje („firmy” oznaczają listę firm, „wyszukiwanie” oznacza szablonowy adres URL do wyszukiwania na liście zasobów, „działy” oznaczają działy bieżącego zasobu)

Poniżej znajduje się przykładowa wymiana HTTP (ciała są w yaml, ponieważ łatwiej jest pisać):

Żądanie

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Odpowiedź: lista linków do głównego zasobu (firmy, ludzie, cokolwiek ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Żądanie: link do firm (przy użyciu body.links.companies poprzedniej odpowiedzi)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

Odpowiedź: częściowa lista firm (poniżej pozycji), zasób zawiera powiązane linki, takie jak link do uzyskania następnych kilku firm (body.links.next) inny (szablonowy) link do wyszukiwania (body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Tak więc, jak widzisz, jeśli korzystasz z linków / relacji, sposób, w jaki tworzysz ścieżkę, część twoich adresów URL nie ma żadnej wartości dla twojego klienta API. A jeśli komunikujesz strukturę swoich adresów URL swojemu klientowi jako dokumentację, to nie robisz REST (lub przynajmniej nie Poziom 3 zgodnie z „ modelem dojrzałości Richardsona ”)


7
„Jak ładny / zrozumiały jest adres URL w interfejsie API REST, jest interesujący tylko jako programista interfejsu API, a nie klient interfejsu API, tak jak nazwa zmiennej w kodzie”. Dlaczego to NIE byłoby interesujące? Jest to bardzo ważne, jeśli ktoś oprócz ciebie również korzysta z interfejsu API. Jest to część doświadczenia użytkownika, więc powiedziałbym, że bardzo ważne jest, aby było to łatwe do zrozumienia dla programistów klientów API. Ułatwienie zrozumienia poprzez wyraźne powiązanie zasobów jest oczywiście premią (poziom 3 w podanym adresie URL). Wszystko powinno być intuicyjne i logiczne z jasnymi relacjami.
Joakim,

1
@Joakim Jeśli tworzysz spoczynkowy interfejs API poziomu 3 (hipertekst jako silnik stanu aplikacji), wówczas struktura ścieżki adresu URL absolutnie nie interesuje klienta (o ile jest poprawna). Jeśli nie dążysz do poziomu 3, to tak, jest to ważne i powinno być zgadywalne. Ale prawdziwy REST to poziom 3. Dobry artykuł: martinfowler.com/articles/richardsonMaturityModel.html
redben

4
Sprzeciwiam się stworzeniu interfejsu API lub interfejsu użytkownika, który nie jest przyjazny dla ludzi. Poziom 3 lub nie, zgadzam się, że łączenie zasobów to świetny pomysł. Ale zasugerowanie tego, że „umożliwia zmianę schematu URL”, oznacza brak kontaktu z rzeczywistością i sposób, w jaki ludzie używają API. To zła rekomendacja. Ale na pewno w najlepszych ze wszystkich światów wszyscy byliby na REST na poziomie 3. Dołączam hiperłącza ORAZ używam zrozumiałego dla ludzi schematu URL. Poziom 3 nie wyklucza tego pierwszego i moim zdaniem POWINIEN dbać. Dobry artykuł :)
Joakim,

Oczywiście należy dbać o łatwość utrzymania i inne obawy, myślę, że nie rozumiesz sedna mojej odpowiedzi: sposób, w jaki wygląda adres URL, nie zasługuje na dużo myślenia i powinieneś „po prostu dokonać wyboru i trzymać się go / być spójne ”, jak powiedziałem w odpowiedzi. A w przypadku interfejsu API REST, przynajmniej moim zdaniem, przyjazność dla użytkownika nie znajduje się w
adresie

9

Nie zgadzam się z tego rodzaju ścieżką

GET /companies/{companyId}/departments

Jeśli chcesz dostać działy, myślę, że lepiej jest użyć zasobu / departments

GET /departments?companyId=123

Przypuszczam, że masz companiestabelę i departmentstabelę, a następnie klasy, aby zmapować je w używanym języku programowania. Zakładam również, że działy mogą być przyłączone do innych podmiotów niż firmy, więc zasób / działy jest prosty, wygodnie jest mieć zasoby odwzorowane na tabele, a także nie potrzebujesz tylu punktów końcowych, ponieważ możesz ponownie użyć

GET /departments?companyId=123

na przykład do dowolnego wyszukiwania

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Jeśli chcesz utworzyć dział,

POST /departments

należy użyć zasobu, a treść żądania powinna zawierać identyfikator firmy (jeśli dział można powiązać tylko z jedną firmą).


1
Dla mnie jest to akceptowalne podejście tylko wtedy, gdy zagnieżdżony obiekt ma sens jako obiekt atomowy. Jeśli nie są, naprawdę nie ma sensu ich rozdzielać.
Simme,

Oto, co powiedziałem, jeśli chcesz również mieć możliwość pobierania działów, co oznacza, że ​​użyjesz punktu końcowego / departments.
Maxime Laval,

2
Sensowne może być również uwzględnienie działów poprzez leniwe ładowanie podczas pobierania firmy, np. GET /companies/{companyId}?include=departmentsPonieważ pozwala to na pobranie zarówno firmy, jak i jej działów w jednym żądaniu HTTP. Fractal robi to naprawdę dobrze.
Matthew Daly

1
Podczas konfigurowania acls prawdopodobnie chcesz ograniczyć /departmentspunkt końcowy, aby był dostępny tylko dla administratora, i każda firma ma dostęp do swoich działów tylko przez `/ company / {companyId} / departments`
Cuzox

@MatthewDaly OData robi to również ładnie z $ expand
Rob Grant

1

Rails zapewnia rozwiązanie tego problemu: płytkie zagnieżdżanie .

Myślę, że jest to dobre, ponieważ w przypadku bezpośredniego kontaktu ze znanym zasobem nie ma potrzeby korzystania z zagnieżdżonych tras, co omówiono w innych odpowiedziach tutaj.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.