caf4fd80d1
Demo ma być rozdawane szeroko i różnym osobom, więc pierwsza wersja — jedno konto na produkcyjnej warstwie danych — nie nadawała się do użycia: każdy dostawałby dostęp do oryginalnych baz, a wgrania jednego klienta widzieliby wszyscy. IZOLACJA OD PRODUKCJI. Warstwa danych i logiczna demo są osobne (manifesty w repo deploy). Osobna musi być TEŻ LOGICZNA, bo zna ona jeden adres warstwy danych — demo korzystające z produkcyjnej logiki i tak trafiłoby na produkcyjne bazy. PULE PER KONTO w warstwie danych. Zapytanie i lista plików niosą nazwę puli; puste = cały udział, czyli produkcja działa dokładnie jak dotąd i o pulach nic nie wie. Nazwa puli przechodzi przez sito dopuszczające wyłącznie znaki bezpieczne w nazwie katalogu — „../..” albo ukośnik wyprowadziłyby zapytanie wprost do cudzych baz, więc sito ZAMIENIA podejrzane znaki zamiast ufać, że nikt ich nie poda. PULA MUSI BYĆ W KLUCZU CACHE ZAPYTAŃ. Bez tego wynik policzony dla jednego konta trafiłby z cache do drugiego — cicha wymiana treści baz między klientami, niewidoczna w logach i nie do wykrycia z zewnątrz. Osobny test tego pilnuje. PULA WYNIKA Z LOGINU, nigdy z żądania. Klient warstwy logicznej jest budowany per żądanie i związany z pulą zalogowanej osoby; gdyby nazwa przychodziła z formularza, wystarczyłoby podstawić cudzy login. Test wysyła `tenant`, `user` i `login` w polach formularza i sprawdza, że nie mają na nią wpływu. Pulę wstrzykujemy w INSTANCJĘ klienta, nie w sygnatury metod. Argumentem trzeba by ją przeprowadzić przez protokół DataSource i build_report — kod, który o kontach nie ma prawa nic wiedzieć — a każde nowe wywołanie byłoby okazją, żeby o nią zapomnieć i sięgnąć nie tam. Konta demo to lista `login:sekret` (DEMO_USERS), bo jedno wspólne konto oznaczałoby wspólną pulę. Format i skrypt haseł te same, co w głównej aplikacji. Pula klienta to JEDEN KATALOG, więc przejście na pełną wersję nie oznacza utraty wgrań — procedurę importu opisuje runbook w repo deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50 lines
2.0 KiB
Python
50 lines
2.0 KiB
Python
"""Kontrakt danych warstwy bazodanowej.
|
|
|
|
Te modele są JEDYNYM publicznym interfejsem tej warstwy. Warstwa logiczna zna
|
|
wyłącznie te kształty (poprzez HTTP/JSON) — nie wie nic o Excelu, cache ani SQL.
|
|
Dzięki temu można podmienić implementację (Excel -> SQL) bez zmiany pozostałych
|
|
warstw.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class SearchQuery(BaseModel):
|
|
"""Znormalizowane zapytanie wyszukiwania przychodzące z warstwy logicznej."""
|
|
|
|
key: str = Field(..., description="Pole/kolumna kanoniczna, po której szukamy, np. 'name'.")
|
|
value: str = Field(..., description="Szukana wartość.")
|
|
exact: bool = Field(False, description="Dopasowanie dokładne vs. zawieranie (contains).")
|
|
# Górny limit celowo niski: to zapytanie oddaje SUROWE wiersze baz, więc wysoki
|
|
# pułap zamienia je w narzędzie do masowego pobrania (LOG-32). 5000 = tyle, ile
|
|
# realnie potrzebuje build_report na jeden obiekt.
|
|
limit: int = Field(50, ge=1, le=5000)
|
|
fields: list[str] | None = Field(
|
|
None, description="Lista pól kanonicznych do zwrócenia; None = wszystkie."
|
|
)
|
|
# PULA KONTA (PRE-29). Puste = cały udział, czyli zachowanie produkcji, która
|
|
# o pulach nic nie wie. Ustawione = wyszukiwanie widzi WYŁĄCZNIE podkatalog
|
|
# tego konta. Używa tego wersja demo, gdzie każde konto ma własny, niewidoczny
|
|
# dla pozostałych zbiór baz.
|
|
tenant: str = Field("", description="Pula plików konta; puste = cały udział.")
|
|
|
|
|
|
class SearchResult(BaseModel):
|
|
"""Wynik wyszukiwania zwracany w górę do warstwy logicznej."""
|
|
|
|
rows: list[dict[str, Any]]
|
|
total: int
|
|
elapsed_ms: float
|
|
cache: str = Field("miss", description="hit/miss/partial — skąd pochodzą dane.")
|
|
provider: str = Field(..., description="Nazwa aktywnej implementacji, np. 'excel' lub 'sql'.")
|
|
|
|
|
|
class HealthInfo(BaseModel):
|
|
status: str = "ok"
|
|
provider: str
|
|
indexed_files: int = 0
|
|
details: dict[str, Any] = Field(default_factory=dict)
|