Init: RoggioApp Architecture, Prisma Schema, API MVP

This commit is contained in:
Clara Zetkin
2026-04-26 19:42:42 +02:00
commit 193b29e8a9
5256 changed files with 1446953 additions and 0 deletions
Vendored Executable
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
Vendored Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# RoggioApp - Vision & Kontext
## Die Grundidee (Ganz von vorne)
* **Use-Case:** Verwaltung einer Ferienwohnungs-Vermietung.
* **Betreiber-Struktur:** Das System wird nicht von einer klassischen "Firma" oder einem "Hotel-Manager" bedient, sondern von einem **Kollektiv**.
## Wichtige Implikationen (Claras Notizen)
* *Rechte & Rollen:* Kollektiv bedeutet oft: flachere Hierarchien, geteilte Verantwortung, Transparenz. Ein starrer Admin/User-Schnitt (wie im alten System) passt hier vielleicht nicht. Jeder im Kollektiv muss vermutlich sehen, wer wann da ist und welche Aufgaben anstehen (z.B. Putzen, Check-In).
* *Gäste-Kommunikation:* Die Interaktion mit den Gästen ist wahrscheinlich persönlicher oder kollaborativer als in einem Standard-Hotel.
+24
View File
@@ -0,0 +1,24 @@
# RoggioApp - Real World & Ökosystem
## 1. Die physische Welt (Real World / RW)
Das System muss eine flexible, organisch wachsende Infrastruktur abbilden können:
* **Gelände/Grundstück:** Viel Fläche, die sich entwickelt.
* **Bestand:**
* 2 feste Häuser.
* 1 alter Zirkuswagen.
* **Zukunft / Flexibilität:**
* Camping-Möglichkeiten.
* Platz für zukünftige Unterkünfte.
* **Nutzungseinheiten (Granularität):**
* In den Häusern gibt es verschiedene Wohnungen.
* Es gibt Funktionsräume und Gemeinschaftsräume/Flächen.
* *Kernanforderung:* Diese Einheiten müssen in verschiedenen Kombinationen vermietet oder belegt werden können (z.B. Wohnung A + Gemeinschaftsraum, oder nur Zirkuswagen).
## 2. Die digitale Welt (Ecosystem)
Die neue Applikation existiert nicht im luftleeren Raum, sondern ist Teil einer Dreifaltigkeit:
1. **Werbung & Frontend (Wordpress):** Externe Website für Marketing, Gästeinformationen und als Einstiegspunkt für Buchungsanfragen.
2. **Dateien & Kommunikation (Nextcloud):** Bestehende Instanz für interne Datenverwaltung und Team-Kommunikation.
3. **Verwaltung (RoggioApp):** Die neu zu bauende Kern-Applikation für Buchungen, Aufgaben (Tasks/Cleaning) und Finanzen.
## 3. Architektur-Ideen & Schnittstellen
* **Single Sign-On (SSO):** Nextcloud als zentrales Auth-Backend (OIDC / SAML) für alle Tools, um die Benutzerverwaltung für das Kollektiv zentral an einem Ort zu halten.
+25
View File
@@ -0,0 +1,25 @@
# RoggioApp - Datenarchitektur & Normalisierung
## 1. Die Grundphilosophie (Single Source of Truth)
* **Keine Redundanz:** Keine doppelte Datenhaltung und (noch wichtiger) keine doppelte Dateneingabe.
* **Ableitung statt Hardcoding (Computed Values):**
* Die Fläche einer Wohnung wird *nicht* als eigenes Feld in der Datenbank eingetragen. Sie wird berechnet (Summe der Flächen aller Räume, die zur Wohnung gehören).
* Die Schlafplatzkapazität einer Einheit wird *nicht* als Zahl eingetragen. Sie ergibt sich aus: `Einheit -> hat Räume (Kategorie: Schlafzimmer) -> hat Betten -> Bett hat Schlafplätze (1 oder 2) = Summe X`.
## 2. Granularität & Hierarchie
Alles im physischen Raum ist eine "Unit" (Einheit), die zueinander in Beziehung steht und strikt kategorisiert sein muss.
**Beispiel-Hierarchie für Kapazitäten:**
1. **Unit: Wohnung** (z.B. Haus A, Wohnung 1)
2. --> besteht aus **Unit: Raum** (z.B. Zimmer 1, Kategorie: Schlafzimmer)
3. ----> enthält **Inventar/Unit: Bett** (Kategorie: Doppelbett, Kapazität: 2 Personen)
4. --> besteht aus **Unit: Raum** (z.B. Zimmer 2, Kategorie: Schlafzimmer)
5. ----> enthält **Inventar/Unit: Bett** (Kategorie: Einzelbett, Kapazität: 1 Person)
*Ergebnis:* Das System weiß automatisch, dass die Wohnung 3 Schlafplätze hat. Wenn ein Einzelbett ins Zimmer gestellt wird, erhöht sich die Kapazität der Wohnung automatisch auf 4, ohne dass jemand das Datenfeld "Wohnung_MaxPax" ändern muss.
## Claras Architektonische Ableitung
Diese Anforderung schreit nach einem strikten, relationalen Baum-Modell (Tree/Graph) in PostgreSQL.
* *Vorteil:* Absolute Konsistenz. Es gibt keine Widersprüche (z.B. "Wohnung hat 5 Betten, aber im System steht Kapazität 2").
* *Herausforderung (auf die wir achten müssen):* Solche "Bottom-Up"-Berechnungen (Summen aus der Tiefe der Datenbank holen) können in SQL sehr komplex (viele JOINs) werden, wenn man z.B. nur schnell filtern will: "Zeig mir alle Wohnungen für 4 Personen".
* *Lösungansatz:* Wir nutzen das normalisierte Modell für die *Struktur*, aber arbeiten beim Abfragen vielleicht mit Datenbank-Views (Materialized Views) oder Prisma-Computed-Fields, damit das Frontend nicht endlos rechnen muss.
+46
View File
@@ -0,0 +1,46 @@
# RoggioApp - Die Ultimative Abstraktion (Einheiten & Eigenschaften)
## 1. Die physische Welt ist ein Graph aus "Einheiten" (Units)
Es gibt keine festen, harten Tabellen wie `Wohnungen`, `Räume` oder `Parkplätze`.
Alles ist eine grundlegende **Unit** (Einheit) auf dem Gelände.
### Was definiert eine Unit?
Eine Unit ist lediglich ein Knotenpunkt (Node), der über seine Eigenschaften (Properties/Traits) und seine Verbindungen (Parent/Child) definiert wird.
**Beispiele für Einheiten (Units):**
* Das Grundstück selbst (Unit)
* Das Haus A (Unit, liegt auf dem Grundstück)
* Ein Zirkuswagen (Unit)
* Ein Badezimmer (Unit, liegt in Haus A)
* Ein Stück Rasen (Unit)
### Die Eigenschaften (Traits / Properties)
Was die Unit *ist* oder was man mit ihr *tun* kann, wird über zugewiesene Eigenschaften bestimmt:
* `is_rentable` (RentalUnit): Die Einheit kann gebucht werden (z.B. Wohnung, Zirkuswagen, Veranstaltungsraum, Parkplatz).
* `needs_service` (Serviceable): Die Einheit erfordert Reinigung oder Instandhaltung (gilt für Mietobjekte, aber eben auch für Flure oder den privaten Putzraum).
* `has_capacity` (Capacity): Definiert, wie viele Schlafplätze (oder Sitzplätze beim Veranstaltungsraum) sich durch die Untereinheiten ergeben.
## 2. Die Akteure (Personen & Gruppen)
Auch hier wird stark normalisiert, um Doppelungen bei den Personendaten zu vermeiden (z.B. wenn Peter dieses Jahr mit Familie A kommt und nächstes Jahr mit Verein B).
* **Person (Die Basis):**
* *Eindeutig / Fest:* Vorname, Nachname, Geburtsdatum, Ausweisnr., Mail, Mobil, Foto.
* *Kategorisierung (Eigenschaft):* Ist diese Person Kollektiv-Mitglied, Support-Gang, Personal oder Gast/Kunde? (Kann sich überlappen!).
* **Adresse (Die Location):**
* *Nicht eindeutig / Shared:* Straße, Hausnummer, PLZ, Stadt, Land. (Mehrere Personen können dieselbe Adresse referenzieren).
* **Gruppe (Die Entität, die bucht):**
* Besteht aus $N$ Personen.
* *Variabel:* Person X kann Mitglied in Gruppe A (Familienurlaub) und Gruppe B (Yoga-Retreat) sein.
## 3. Die Transaktionen (Buchungen & Aktionen)
* **Buchungen (Bookings):**
* Verbinden eine **Gruppe** mit einer **RentalUnit** (Unit mit `is_rentable=true`) über einen bestimmten Zeitraum.
* **Aktionen (Tasks / Operations):**
* Putzen, Einkaufen, Abholen.
* Können an eine Buchung geknüpft sein (Endreinigung nach Gruppe A).
* Oder an eine Unit (Grundreinigung des Flurs, der `needs_service` ist, aber nicht `is_rentable`).
## Claras architektonische Ableitung
Das ist ein Entity-Component-System (ECS) oder ein Node-Graph, wie man ihn aus Game-Engines oder extrem skalierbaren ERP-Systemen kennt!
Statt 20 verschiedener Tabellen (`flats`, `rooms`, `parking_spaces`) bauen wir einen großen `units`-Baum. Was die Unit kann, bestimmt eine Verknüpfungstabelle `unit_traits`.
Das macht das System zukunftssicher: Wenn ihr morgen beschließt, Surfbretter zu verleihen, legt ihr einfach neue Units an, gebt ihnen den Trait `is_rentable` (RentalUnit) und sie tauchen sofort im Buchungskalender auf, ohne dass das Datenbankschema geändert werden muss.
+34
View File
@@ -0,0 +1,34 @@
# RoggioApp - Events, Aufgaben & Relationen
## 1. Die Universalität der Entitäten
Nicht nur die physische Welt (Räume, Zirkuswagen), sondern **alles** ist eine grundlegende Entität, die über Eigenschaften (Traits) und Verknüpfungen definiert wird.
* **Personen als Entitäten:** Können Rollen in Gruppen übernehmen.
* **Dynamische Relationen (Verknüpfungen mit Eigenschaften):** Eine Person ist nicht einfach "in" einer Gruppe. Die Verbindung selbst hat eine Eigenschaft!
* *Person A* ist über die Eigenschaft `is_comm_contact` mit *Gruppe X* verbunden.
* *Person B* ist über die Eigenschaft `is_finance_contact` mit *Gruppe X* verbunden.
## 2. Buchungen und Aufgaben = "Events"
Die höchste Form der Abstraktion betrifft die Zeit und die Aktionen. Es gibt keine starren Tabellen für "Buchungen" und "Putzpläne". Beide sind im Kern **Events**.
### Was ist ein Event?
Ein Event ist ein Knotenpunkt in der Zeit, der andere Entitäten miteinander verbindet und einen Zustand hat.
* **Beispiel A: Die Buchung (Event-Typ: Rental)**
* *Eigenschaften des Events:* Zeitraum (14.08 - 21.08), Status (Bestätigt).
* *Relation 1:* Verknüpft mit Entität "Gruppe Familie Rossi" (Wer?).
* *Relation 2:* Verknüpft mit Entität "Haus A" + "Zirkuswagen" (Wo?).
* *Relation 3:* Finanzen/Rechnung (Geld).
* **Beispiel B: Das Putzen (Event-Typ: Task/Maintenance)**
* *Eigenschaften des Events:* Fälligkeitsfenster (21.08 10:00 - 15:00), Status (Offen).
* *Relation 1:* Verknüpft mit Entität "Haus A" (Was muss geputzt werden?).
* *Relation 2:* Verknüpft mit Entität "Kollektiv-Mitglied Sarah" (Wer putzt?).
* *Spezial-Relation (Event -> Event):* Getriggert durch / Abhängig von "Event A (Buchung Ende)".
## Claras architektonische Ableitung
Das gesamte System besteht eigentlich nur aus drei riesigen Säulen:
1. **Nodes (Entitäten):** Die Bausteine der Welt (Personen, Einheiten, etc.).
2. **Edges (Relationen/Traits):** Wie die Nodes verbunden sind und was sie können.
3. **Events (Zeitliche Knoten):** Das Klebeband, das Nodes auf einer Zeitachse zusammenführt (Buchungen, Aufgaben, Wartung).
*Herausforderung für die Datenbank:* So etwas baut man idealerweise mit einer Graphendatenbank (wie Neo4j) oder in PostgreSQL mit JSONB-Feldern für die Traits und extrem klugen Pivot-Tabellen (Edge-Tables) für die Beziehungen, damit man nicht den Verstand beim Queries-Schreiben verliert. Wir müssen einen Weg finden, das in Prisma sauber und typsicher zu modellieren, ohne die Performance zu töten.
+27
View File
@@ -0,0 +1,27 @@
# RoggioApp - Architektur & Schnittstellen-Strategie
## 1. Das Kern-System (Die absolute Source of Truth)
Das Ziel ist eine unantastbare, zukunftssichere und hochflexible Datenstruktur, die Schnittstellen (Frontends, CMS, Apps) als austauschbare Plugins betrachtet.
* **Der Datenbank-Core:**
* *Relationales Fundament (SQL):* Für absolute Konsistenz bei Transaktionen, Buchungen, Finanzen und den Kern-Entitäten (Nodes/Edges). Postgres ist hierfür ideal.
* *NoSQL-Einflüsse / Flexibilität:* Eigenschaften (Traits/Properties), die sich ständig ändern können, werden als JSONB-Felder (in Postgres) oder in einer kooperierenden NoSQL-Struktur abgelegt.
* *Caching/Performance:* Redis oder Memory-Caches vor den Berechnungen, damit der komplexe Graph nicht bei jedem Aufruf die SQL-DB auslastet.
* *Die API:* Eine zentrale REST- oder GraphQL-API. *Nichts* redet direkt mit der Datenbank, alles geht durch diese API.
## 2. Die Satelliten (Schnittstellen & Konsumenten)
Um dieses Kern-System kreisen verschiedene spezialisierte Werkzeuge, die über die API kommunizieren:
### A. Nextcloud (Das interne Werkzeug)
* **Auth (RBAC):** Nextcloud fungiert als Identity Provider (OIDC). Rollen und Rechte des Kollektivs werden dort gepflegt.
* **Ablage:** Dokumente, Rechnungs-PDFs und Verträge werden in Nextcloud gespeichert (das Roggio-Backend speichert nur den Link/die WebDAV-Referenz).
* **Organisation:** Tasks (Putzen/Support) und interne Kalender-Syncs (CalDAV) für das Kollektiv.
### B. WordPress (Das Schaufenster)
* **Aufgabe:** Marketing, Landing Pages, SEO.
* **Integration:** Ein schlankes Plugin oder Widget greift per API auf das Roggio-Backend zu, liest die Verfügbarkeiten aus (Belegungskalender) und schickt Buchungsanfragen zurück ins Kern-System.
### C. WebApp / MobileApp (Das Cockpit)
Eine (oder zwei getrennte) maßgeschneiderte Frontend-Applikationen (vermutlich React/React Native), die über die API mit dem Kern sprechen:
* **Für das Kollektiv / Personal:** Das "Backend-Frontend". Ansicht von Rechnungen, Zuweisen von Tasks, Managen der Graphen (Einheiten erstellen).
* **Für die Gäste (Kunden):** Eine Art "Guest-Portal" (Self-Service). Rechnungen einsehen, Services hinzubuchen (z.B. Sauna), Chat/Nachrichten, Check-in-Infos.
+44
View File
@@ -0,0 +1,44 @@
# RoggioApp - Tech-Stack Empfehlung (Hetzner, Schmal, API-First)
## Prämissen
1. **Hetzner-Ökosystem:** Performant, günstig, viel Bandbreite. Eigene VMs via Hetzner Cloud (Console) sind in Sekunden da.
2. **So schmal wie möglich (Lean):** Keine Over-Engineering-Monster (wie Kubernetes), wenn Docker Compose reicht. Wenig Wartungsaufwand für das Kollektiv.
3. **Zukunftssicher & Flexibel:** API-First, Graph-Datenmodell (ECS-Ansatz), Integration mit Nextcloud (WebDAV/OIDC) & WordPress.
## Der empfohlene Tech-Stack
### 1. Infrastruktur & Hosting (Die Basis)
* **Server:** Hetzner Cloud (z.B. CX22 oder CPX21). Reicht völlig für den Anfang, skaliert in Sekunden.
* **Orchestrierung:** **Docker Compose**. Kein K8s, kein Swarm. Eine einfache `docker-compose.yml`, die alle Core-Services hochzieht. Das ist extrem wartungsarm und portabel.
* **Reverse Proxy:** **Nginx Proxy Manager** (oder Caddy). Extrem schmal, kümmert sich automatisch um Let's Encrypt SSL-Zertifikate und leitet Traffic auf die Docker-Container (wie wir es lokal schon erfolgreich getestet haben!).
### 2. Die Datenhaltung (Das Herzstück)
* **Datenbank:** **PostgreSQL**.
* *Warum:* Absoluter Industrie-Standard, bombensicher für Finanz-Transaktionen und durch die exzellente `JSONB`-Unterstützung perfekt für unser flexibles "Traits/Eigenschaften"-Modell.
* **Caching / Message Broker (Optional, erst später zuschalten):** **Redis**.
* *Warum:* Wenn später WordPress alle 2 Sekunden den Kalender anfragt, puffert Redis das, damit Postgres nicht schwitzt. Für den Start (MVP) können wir das aber noch weglassen ("so schmal wie möglich").
### 3. Das Backend (Die API)
* **Sprache:** **Node.js mit TypeScript**.
* *Warum:* Enorme Community, sehr schnell entwickelt, und (das ist der Hauptgrund) wir können Code und Typen zwischen Frontend und Backend teilen!
* **ORM (Datenbank-Mapper):** **Prisma**.
* *Warum:* Es gibt nichts Besseres für TypeScript und Postgres. Es generiert uns typsichere Abfragen, verwaltet die Datenbank-Migrationen und macht das Jonglieren mit unserem Graph/Node-Modell deutlich angenehmer als rohes SQL.
* **Framework:** **NestJS** (oder leichtgewichtig: **Hono / Express**).
* *Empfehlung für euch:* Wenn es sauber und "Enterprise-like" strukturiert sein soll: *NestJS*. Wenn es absolut schmal und pfeilschnell (Edge-ready) sein soll: *Hono*.
### 4. Die Frontends (Das Gesicht)
* **Core-App (Kollektiv & Gäste Web-Portal):** **React** (gebaut mit **Vite** oder **Next.js**).
* *Warum:* Absoluter Standard. Riesiges Ökosystem an Kalender- und UI-Bibliotheken (z.B. Tailwind CSS, shadcn/ui).
* *Architektur:* Eine Single-Page-Application (SPA) oder statisch exportierte Seite, die direkt mit dem Node-Backend redet.
* **Marketing (bestehend):** WordPress (bei Hetzner Webhosting).
* *Warum:* Ihr habt es schon, jeder kann Inhalte einpflegen. Zieht die Buchungsdaten über eine simple REST-Schnittstelle von unserem Node-Backend.
* **Dateien / Auth (bestehend):** Nextcloud (bei Hetzner Storage Share).
* *Integration:* Das Node-Backend authentifiziert sich per OAuth2/OIDC an Nextcloud und speichert Rechnungen per WebDAV dort ab.
## Zusammenfassung des MVP-Setups (Schmalster Weg)
1. **Eine Hetzner Cloud VM**
2. Darauf läuft **Docker Compose**
3. Darin laufen genau 3 Container:
- **Nginx Proxy Manager** (für SSL und Domains)
- **PostgreSQL** (Die Datenbank)
- **Node.js (Hono/Prisma)** (Das Backend + liefert das kompilierte React-Frontend aus)
+30
View File
@@ -0,0 +1,30 @@
# RoggioApp - Langzeit-Vision & Hardware-Schnittstellen
## 1. Lokale Resilienz (Datenbank-Mirroring)
* **Ziel:** Die Cloud (Hetzner) darf ausfallen oder das Internet auf dem Gelände streiken, ohne dass der Betrieb komplett lahmgelegt wird.
* **Ansatz:** Ein lokales Mirror-System für die Datenbank.
* *Architektur-Implication:* PostgreSQL erlaubt logische Replikation (Master-Slave oder Master-Master Konzepte). Wir sollten das Backend so bauen, dass es (später) Offline-First / Sync-Fähigkeiten bekommt oder zumindest ein lokaler Read-Replica Server auf dem Gelände läuft. (Für den Start bauen wir Single-Node, halten uns aber die Multi-Node Replikation in Postgres offen).
## 2. Zentrale User-Verwaltung (Identity & Access Management - IAM)
* **Ziel:** Das Auth-System (Nextcloud / OIDC) soll in Zukunft der einzige Schlüssel für *alles* sein.
* **Geplante Erweiterungen für User:**
* WireGuard VPN-Profile
* VoIP / SIP-Accounts (Telefonie)
* E-Mail Accounts
* NFC / RFID Zugangskontrolle (Türen)
* *Architektur-Implication:* Nextcloud als OIDC-Provider ist hier bereits ein sehr guter Startpunkt. Wir dürfen in der RoggioApp keine eigenen, isolierten "User-Passwort"-Tabellen bauen. Die RoggioApp referenziert nur eine globale `user_id` (z.B. aus LDAP oder Nextcloud), an der die Hardware-Schlüssel hängen.
## 3. Home Automation & IoT (Der Campus)
* **Ziel:** Smarte Steuerung und Überwachung der physischen Infrastruktur vor Ort.
* **Core-Technologien:** HomeAssistant, MQTT, LoRaWAN.
* **Kritische Systeme:**
* PV / Solar Management (Strom)
* Wasserspeicher (Infrastruktur)
* Zugangskontrolle (Smart Locks)
* *Architektur-Implication:* Unser Konzept der "Units" und "Traits" greift hier perfekt!
* Eine Unit (z.B. Zirkuswagen) bekommt in unserer Datenbank den Trait `has_smart_lock`.
* In diesem Trait speichern wir die MQTT-Topic-ID (`/house/zirkuswagen/lock`).
* Wenn im Event "Buchung" der Status auf "Check-in" wechselt, feuert unser Node-Backend einen MQTT-Befehl an den lokalen HomeAssistant, um den Code des Gastes auf das Schloss zu spielen.
## Claras Fazit
Die Weichenstellung ist klar: Das System ist kein reines "Software-Tool", sondern das Betriebssystem für ein physisches Gelände. Die strikte Trennung von Core-Logik, externem Auth-Provider und MQTT-fähigen Hardware-Layern sichert uns ab. Keine dieser Anforderungen sprengt unseren API-First / Node.js / Postgres Stack im Gegenteil, MQTT und OIDC lassen sich in Node.js hervorragend nativ integrieren.
+8
View File
@@ -0,0 +1,8 @@
# RoggioApp - Nächster Schritt: Das Prisma Datenmodell (MVP)
Um vom Konzept in die Praxis zu kommen, sollten wir als Nächstes ein `schema.prisma` entwerfen, das unseren "Graph aus Units und Events" abbildet.
*Zu klären:*
1. Wie bilden wir die flache "Unit"-Tabelle ab?
2. Wie speichern wir die flexiblen "Traits" (als JSONB oder eigene Tabellen)?
3. Wie verknüpfen wir Events (Buchungen/Aufgaben) typsicher?
+12
View File
@@ -0,0 +1,12 @@
# RoggioApp - System-Status & Paranoia-Checkpoint
## 1. Warum wir sicher sind (Keine Angst vor Token-Limits)
* **Festplatten-Gedächtnis:** Alles, was wir bisher konzipiert haben (Die Node/Graph-Abstraktion, die Architektur-Entscheidungen), habe ich bereits als Markdown-Dateien in diesem Ordner abgelegt (`01_...` bis `09_...`).
* **Infrastruktur existiert wirklich:** Die Docker-VM, der SMB-Share, das Let's Encrypt Zertifikat, die Prisma-Konfiguration und die laufende PostgreSQL-Datenbank sind **echt**. Sie laufen unabhängig von diesem Chatfenster.
* **Der Code liegt auf der Platte:** Die `schema.prisma` und die Nginx-Konfigurationen sind nicht flüchtig im Chat-Verlauf, sondern fest gespeichert.
## 2. Wie wir nach einem Neustart (oder morgen) weitermachen
Sollten wir morgen neu anfangen oder das Kontextfenster platzen, sagst du einfach:
*"Clara, schau in den Ordner `/home/clara/.openclaw/workspace/projects/RoggioApp/Brainstorm/260426/`, lies dir die Architektur-Entscheidungen durch und lass uns am Prisma-Backend weiterarbeiten."*
Ich werde dann genau dort ansetzen, wo wir aufgehört haben, weil mein "Gedächtnis" auf SSD-Basis (in diesen Dateien) liegt, nicht nur im flüchtigen Chat-Protokoll.
+36
View File
@@ -0,0 +1,36 @@
# RoggioApp - Personen Entitäten & Nextcloud Sync (OIDC)
## Das Problem
Wir wollen Personen (`Persons`) als zentrale Knoten im Graph (Datenbank) modellieren.
Aber die "Wahrheit" über den Login (Passwort, Rollen) soll in der Nextcloud (als Identity Provider) liegen.
Stellen wir uns ein Bein, wenn wir in der Datenbank jetzt Personen anlegen, ohne dass die Nextcloud steht?
## Die Lösung: Entkopplung von "Identity" und "Profile"
Wir trennen das Konzept "Wer bist du technisch?" von "Wer bist du in der realen Welt?".
### 1. Das Roggio-Profil (Die Person-Entität)
In unserer Postgres-DB (Prisma) liegt das Modell `Person`.
Dieses Modell enthält **nur Profil- und Geschäftsdaten**:
* `firstName`, `lastName`, `phone`
* `traits` (JSONB) für alles andere (Körpergröße für Leihräder, Essensvorlieben für Gruppen).
* **Der Anker:** Ein Feld `ssoId` (Single-Sign-On ID).
### 2. Die Nextcloud-Identität (Der Identity Provider)
In der Nextcloud liegen die sensiblen Login-Daten:
* `username` (z.B. "sev")
* `password` (gehasht)
* `email`
* `groups` (z.B. "admin", "kollektiv")
### Der Workflow (Wie beides zusammenspielt)
Wir können jetzt sofort Personen in der Datenbank anlegen (z.B. Gäste, die sowieso keinen Nextcloud-Account bekommen).
Wenn sich später ein Kollektiv-Mitglied über Nextcloud einloggt (OIDC), passiert folgendes:
1. Nextcloud sagt: *"Hier ist User 'sev' mit der OIDC-Sub-ID '12345' und der Email 'sev@...'."*
2. Unser Backend schaut in der `Person`-Tabelle nach: *"Habe ich eine Person mit `ssoId = '12345'` oder `email = 'sev@...'`?"*
3. Wenn Ja: Die Person ist authentifiziert und das Roggio-Profil wird mit der Sitzung verknüpft.
4. Wenn Nein (erster Login): Das Backend erstellt automatisch eine neue `Person` mit der `ssoId='12345'`.
## Fazit für die Entwicklung
Wir können das Datenmodell jetzt zu 100% fertigbauen und verfeinern!
Gäste existieren ohnehin nur in Roggio. Für das Kollektiv lassen wir einfach das Feld `ssoId` vorerst leer. Wenn wir Nextcloud anbinden, füllen wir die ID nach oder lassen sie beim ersten Login auto-matchen.
Kein Bein gestellt!
+31
View File
@@ -0,0 +1,31 @@
# RoggioApp - Das Kollektiv als eigene Entität
## Die Erkenntnis: Das Kollektiv ist kein abstrakter "Gott", sondern nur eine Gruppe
Wenn man konsequent abstrahiert, ist die Betreiber-Struktur (das Kollektiv) nichts anderes als eine ganz normale Entität im Graphen.
### 1. Das Kollektiv ist eine `Group`
In unserer Datenbank-Tabelle `Group` existieren Gruppen für "Familie Rossi" (Gäste).
Genau dort existiert auch ein Datensatz für "Das Kollektiv".
Unterschieden wird dies nur durch das JSONB-Feld `traits`:
* *Familie Rossi:* `{"group_type": "guest", "preferences": "quiet"}`
* *Das Kollektiv:* `{"group_type": "operator", "bank_account": "DE123...", "tax_id": "DE999..."}`
### 2. Die Mitglieder sind `Person`-Entitäten mit Relationen
Die Menschen, die das System betreiben (Sev, Sarah, etc.), sind normale `Person`-Einträge.
Ihre Zugehörigkeit zum Kollektiv wird durch die reguläre Pivot-Tabelle `GroupMembership` definiert:
* *Person ID (Sev)* + *Group ID (Das Kollektiv)* + *Rolle (`finance_admin`)*
* *Person ID (Sarah)* + *Group ID (Das Kollektiv)* + *Rolle (`cleaning_crew`)*
### 3. Warum das absolut genial ist: Mandantenfähigkeit ("Multi-Tenancy")
Wenn wir das Kollektiv als einfache Gruppe speichern, ist das System sofort **mandantenfähig**.
Beispiel: In 5 Jahren wollt ihr neben dem Haupt-Gelände noch ein zweites Haus im Nachbardorf verwalten, das aber von einer anderen Genossenschaft (oder GmbH) betrieben wird.
Wir müssen *keinen einzigen* Datenbankumbau vornehmen! Wir legen einfach eine zweite Operator-Gruppe an, ordnen dieser Gruppe ein paar Personen und die neuen "Units" (Räume) zu, und fertig. Die Rechnungen ziehen sich dann automatisch die Bankdaten von Gruppe B statt von Gruppe A.
## Fazit
Das System kennt keinen "Super-Admin-Hardcode". Es kennt nur den Graphen. Das Kollektiv ist ein normaler Knotenpunkt, der sich über seine Beziehungen definiert.
### 4. B2B / Firmen als Kunden (Organisationen)
Genau wie "Das Kollektiv" eine Operator-Gruppe ist, sind auch B2B-Kunden (Firmen, NGOs, Vereine) einfache Einträge in der Tabelle `Group`.
* *Verein e.V.:* `{"group_type": "b2b_guest", "tax_id": "DE123...", "invoice_notes": "Vereinsrabatt"}`
* Die Verknüpfung der tatsächlichen Gäste erfolgt wieder über `GroupMembership`. (Z.B. ist Peter der `invoice_contact` für den Verein, und Sarah, Tim und Lisa sind `guest`).
* Auch Rechnungen können somit direkt an die Entität `Group` ausgestellt werden, da die Firmen-/Vereinsadresse in deren Traits liegt, anstatt an einer einzelnen Person zu kleben.
+58
View File
@@ -0,0 +1,58 @@
# RoggioApp - Analyse: Die Person-Entität (Hard vs. Flexible)
## Die Herausforderung
Was gehört zwingend als harte, durchsuchbare SQL-Spalte in die Tabelle `Person`, und was lagern wir als flexible Eigenschaft (Trait) in das JSONB-Feld oder in Relationen aus?
## Sevs Entwurf & Claras Analyse
### 1. Originär (Die Person selbst)
* **Vorname & Nachname:** ✅ Absolut notwendig. Harte SQL-Spalten. (Ggf. um `nickname` als Spalte erweitern).
* **Geburtsdatum:** ✅ Harte Spalte (für Altersberechnungen, Kurtaxe, Meldescheine extrem wichtig).
* **Mobile-Nr & Private E-Mail:** ⚠️ *Diskussion:* Sind die eindeutig?
* *Realität:* Oft buchen Ehepaare oder Familien mit nur *einer* gemeinsamen Mailadresse oder Handynummer.
* *Lösung:* Wir dürfen `email` und `phone` **nicht** auf `UNIQUE` (Einzigartig) in der Datenbank setzen! Sie bleiben harte SQL-Spalten für die schnelle Suche, aber Doppelungen sind erlaubt.
### 2. Relational / Flexibel (Die Eigenschaften)
* **Anrede (Pronouns / Title):** -> JSONB (`traits`). (Ändert sich gesellschaftlich, JSONB ist hier flexibler als ein harter SQL-Enum).
* **Identity-Dokument (Perso/Passport):** -> JSONB (`traits`).
* *Warum:* Die Felder variieren stark je nach Land (Ausgestellt in, Gültig bis, Behörde). Ein JSONB-Objekt `identity_document: { type: "passport", number: "...", valid_until: "..." }` ist hier perfekt.
* **Geburtsort:** -> JSONB (`traits`).
* **Adresse (Wohnort):** -> ⚠️ *Eigene Tabelle!*
* *Warum:* Eine Adresse (Straße, Stadt, Land) kann von 5 Familienmitgliedern oder einer Firma geteilt werden. Wir bauen eine Tabelle `Address` und verknüpfen die Person(en) per ID damit. Das spart immense Redundanz.
* **Finanz-Infos (IBAN etc.):** -> JSONB (`traits`).
### 3. Logisch durch den Graphen gelöst (Architektur-Bonus)
* **Buchungsverlauf:** Wird *nicht* in der Person gespeichert. Ergibt sich zu 100% aus den `Event`-Knoten, die mit der Person (oder ihrer Gruppe) verknüpft sind.
* **Loginrights:** Werden nicht gespeichert. Das übernimmt die Nextcloud (SSO). Wir speichern nur die `ssoId`.
* **Type (Kollektiv / Client / Personal):** Wird *nicht* als hartes Feld gespeichert! Ergibt sich aus den `GroupMembership`-Verknüpfungen (z.B. wenn Person A Mitglied in der Operator-Gruppe ist, ist sie Personal).
* **Groups & Groupfunction:** Wird 1:1 durch unsere Pivot-Tabelle `GroupMembership` (inkl. `role`) abgebildet.
## Claras Vorschlag für die finale Prisma-Tabelle `Person`
```prisma
model Person {
id String @id @default(uuid())
firstName String @db.VarChar(100)
lastName String @db.VarChar(100)
nickname String? @db.VarChar(100)
birthDate DateTime? @db.Date
// Kontaktdaten (NICHT UNIQUE!)
email String? @db.VarChar(255)
phone String? @db.VarChar(50)
// Verknüpfung zur Authentifizierung
ssoId String? @unique @db.VarChar(255)
// Die Wohnadresse (Relational)
addressId String?
address Address? @relation(fields: [addressId], references: [id])
// Alles andere (Pass, Finanzen, Anrede, Geburtsort, Allergien)
traits Json @default("{}")
groupMemberships GroupMembership[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
```
+82
View File
@@ -0,0 +1,82 @@
# RoggioApp - Units & Hierarchie (Beispiel: Wohnung im 1. Stock)
## 1. Die Hierarchie (Der Tree)
Die Macht unseres Node-Graphen zeigt sich in der Hierarchie. Wir legen nicht eine fette Zeile in einer Tabelle "Wohnung" an, sondern stapeln Units ineinander (`parentId`).
**Beispiel-Baum:**
* **Unit 1:** Haus A (Parent: Null)
* **Unit 2:** 1. Stock (Parent: Unit 1)
* **Unit 3:** Wohnung Sonnenaufgang (Parent: Unit 2, `is_rentable: true`)
* **Unit 4:** Küche (Parent: Unit 3)
* **Unit 5:** Bad (Parent: Unit 3)
* **Unit 6:** Wohnzimmer (Parent: Unit 3)
* **Unit 7:** Terrasse (Parent: Unit 3)
* **Unit 8:** Schlafzimmer 1 (Parent: Unit 3)
* **Unit 9:** Doppelbett (Parent: Unit 8, `capacity: 2`)
* **Unit 10:** Schlafzimmer 2 (Parent: Unit 3)
* **Unit 11:** Einzelbett A (Parent: Unit 10, `capacity: 1`)
* **Unit 12:** Einzelbett B (Parent: Unit 10, `capacity: 1`)
* **Unit 13:** Schlafzimmer 3 (Parent: Unit 3)
* **Unit 14:** Einzelbett (Parent: Unit 13, `capacity: 1`)
## 2. Die Eigenschaften (Traits im JSONB)
Wie unterscheiden wir nun ein Schlafzimmer von einem Bad, ohne für jedes eine eigene Datenbanktabelle zu bauen? Über das JSONB-Feld `traits`.
**Beispiele für das `traits`-Feld (JSON):**
* **Bei Unit 3 (Wohnung Sonnenaufgang):**
```json
{
"type": "apartment",
"is_rentable": true,
"base_price": 85.00,
"cleaning_fee": 40.00
}
```
*(Hinweis: Fläche und Kapazität stehen hier absichtlich NICHT drin. Sie werden später live aus den Kindern berechnet!)*
* **Bei Unit 8 (Schlafzimmer 1):**
```json
{
"type": "room",
"room_category": "bedroom",
"area_sqm": 18.5,
"features": ["Meerblick", "Verdunkelungsrollos"]
}
```
* **Bei Unit 9 (Doppelbett):**
```json
{
"type": "inventory",
"inventory_category": "bed_double",
"sleep_capacity": 2,
"width_cm": 180
}
```
* **Bei Unit 5 (Bad):**
```json
{
"type": "room",
"room_category": "bathroom",
"area_sqm": 8.0,
"features": ["Dusche", "Badewanne", "Bidet", "Fenster"]
}
```
* **Bei Unit 4 (Küche):**
```json
{
"type": "room",
"room_category": "kitchen",
"area_sqm": 12.0,
"inventory": ["Kühlschrank", "Herd", "Kaffeemaschine", "Töpfe"]
}
```
## 3. Claras Fazit zur Flexibilität
Wir müssen in Postgres nur EINE einzige Tabelle `Unit` abfragen, um das gesamte Gebäude zu kennen.
Wenn jemand wissen will: *"Wie groß ist die Wohnung und wie viele Leute können dort schlafen?"*
Dann fragt unser Backend: *"Summiere alle `area_sqm` von allen Units, deren direkter oder indirekter Parent die 'Wohnung Sonnenaufgang' ist. Und summiere alle `sleep_capacity` der Kinder."*
Ergebnis: Fläche = 38.5qm, Schlafplätze = 5.
+52
View File
@@ -0,0 +1,52 @@
# RoggioApp - Inventar & Zustand (Soll vs. Ist)
## Das Problem
Inventar (Löffel, Gasflaschen) ist nicht statisch. Es verbraucht sich, geht kaputt oder verschwindet. Wir müssen nicht nur wissen, *was* da sein sollte, sondern *wie viel* davon tatsächlich da ist, um Aktionen (Nachbestellen) zu triggern.
## Sevs Ansatz: Soll- vs. Ist-Anzahl
Beispiele:
* Küche: Löffel (Soll: 10, Ist: 5)
* Butanlager: 5kg Flaschen (Soll: 10, Ist: 7)
## Claras architektonische Einordnung im Unit-Graphen
Wir können Inventar nicht einfach als dumme Text-Strings in ein Array (`inventory: ["Löffel"]`) packen, wenn wir Soll/Ist-Zahlen erfassen wollen. Wir müssen dem Inventar eine kleine Struktur geben.
Da wir JSONB (`traits`) verwenden, ist das ein Kinderspiel und erfordert **keine** Änderung am Prisma-Schema!
### Lösung A: Inventar als strukturiertes Objekt im Raum-Trait
Anstatt eines einfachen Arrays speichern wir ein Array aus Objekten im JSONB der Unit (z.B. der Küche oder dem Butanlager).
*Trait für die Unit "Butanlager":*
```json
{
"type": "storage_room",
"inventory_state": [
{
"item_id": "item_butan_5kg",
"name": "5kg Butangas",
"target_qty": 10,
"actual_qty": 7,
"alert_threshold": 3
}
]
}
```
*Vorteil:* Sehr schnell abfragbar.
*Nachteil:* Die Endreinigungskräfte müssten theoretisch die Unit "Butanlager" bearbeiten, um die Zahl anzupassen.
### Lösung B: Inventar als eigene Sub-Units (Der reinrassige Graph-Weg)
Anstatt die Gabeln als Text in die Küche zu schreiben, machen wir aus "Bestand: Löffel" eine eigene Unit, die als Child *unter* der Küche hängt.
*Unit "Löffel-Bestand" (Parent: Küche):*
```json
{
"type": "inventory_item",
"name": "Kaffeelöffel",
"target_qty": 10,
"actual_qty": 5
}
```
*Vorteil:* Absolut sauber. Wenn eine Putzkraft nach der Reinigung einen "Schwund" meldet, erstellt das System ein `Event` ("Inventar-Korrektur") und verknüpft es direkt mit der Unit "Löffel-Bestand". Die Historie bleibt erhalten (Wer hat wann gemeldet, dass 5 Löffel fehlen?).
## Die Konsequenz für Aufgaben (Events)
Wenn `actual_qty` kleiner wird als ein definierter Schwellenwert (z.B. bei Gasflaschen), feuert unser System automatisch ein neues `Event` vom Typ `TASK_BUY` (Einkaufen). Dieses Event taucht dann in Nextcloud auf der Einkaufsliste auf.
+13
View File
@@ -0,0 +1,13 @@
# RoggioApp - Beschluss: Weg 2 (Alles ist eine Unit)
## Der Entschluss
Wir nutzen **Weg 2** für *jedes* Inventar, bei dem Bestände, Schwund oder Nachbestellungen (Soll/Ist) relevant sind.
Das bedeutet: Wir verpacken Inventar nicht in unübersichtliche JSON-Arrays innerhalb eines Raums, sondern machen **jedes relevante Inventar zu einer eigenen Child-Unit**.
## Warum das die mächtigste Entscheidung ist
1. **Historie (Audit-Trail):** Wenn "5 Löffel fehlen" als JSON in der Küche steht, weiß niemand, wann das passiert ist. Wenn der Löffel-Bestand eine eigene Unit ist, kann ein `Event` ("Inventur nach Abreise von Rossi") mit der Löffel-Unit verknüpft werden. Man sieht exakt: Am 26.04. hat Sarah 5 fehlende Löffel eingetragen.
2. **Modularität:** Ein Grill (Unit) kann heute zur Wohnung 1 (Parent) gehören und morgen auf dem Zirkuswagen-Platz (neuer Parent) stehen. Hätten wir den Grill als JSON in Wohnung 1 geschrieben, müssten wir JSON manipulieren. So verschieben wir einfach die `parentId`.
3. **Wartung & Defekte:** Eine Spülmaschine (Unit) kann den Trait `status: "broken"` bekommen. Das generiert automatisch ein `Event` (Task: Reparatur) für die Support-Gang.
## Das Datenmodell bleibt unberührt
Es beweist die Stärke des Modells: Obwohl wir gerade die Verwaltung von Besteck, Gasflaschen und Spülmaschinen beschlossen haben, müssen wir in `schema.prisma` **nichts** ändern. Die `Unit`-Tabelle fängt das komplett ab.
+37
View File
@@ -0,0 +1,37 @@
# RoggioApp - Zeitachsen (Buchungen, Anwesenheit, Schichten)
## Die große Vereinheitlichung
Genau wie bei den Units gibt es auch bei der Zeit keine harten Trennungen.
**Eine Buchung für einen Gast, eine Schicht für Personal und die private Anwesenheit eines Kollektiv-Mitglieds sind technisch absolut identisch.**
Alles sind `Event`-Knoten auf einer Zeitachse.
### Der Aufbau eines Zeitachsen-Events
Jedes dieser Ereignisse nutzt exakt dieselbe Tabelle (`Event`) in der Datenbank. Die Magie entsteht durch die Verknüpfung (Relationen) und das Etikett (Type).
**Beispiel 1: Die klassische Gast-Buchung**
* `type`: "RENTAL"
* `startTime`: 15.08.2026 15:00
* `endTime`: 22.08.2026 10:00
* `targetUnitId`: Haus A (Was ist belegt?)
* `targetGroupId`: Familie Rossi (Wer belegt es?)
**Beispiel 2: Die Personal-Schicht (Putzen)**
* `type`: "SHIFT_WORK"
* `startTime`: 22.08.2026 10:00
* `endTime`: 22.08.2026 14:00
* `targetUnitId`: Haus A (Wo wird gearbeitet?)
* `targetPersonId`: Putzkraft Maria (Wer arbeitet?)
* *Abhängigkeit (`parentEventId`):* Verweist auf Beispiel 1 (Wird automatisch getriggert, wenn Rossi abreist).
**Beispiel 3: Anwesenheit Kollektiv (Workation/Urlaub)**
* `type`: "PRESENCE_COLLECTIVE"
* `startTime`: 01.09.2026
* `endTime`: 14.09.2026
* `targetUnitId`: Zirkuswagen (Wo schläft Sev?)
* `targetPersonId`: Sev (Wer ist da?)
## Warum das Architektur in Perfektion ist
Da alle drei Dinge in *derselben* Tabelle als Event mit Start- und Endzeit liegen, ist das Verhindern von Doppelbelegungen ("Clash-Detection") extrem einfach.
Wenn ein Gast "Haus A" buchen will, fragt die Datenbank: *"Gibt es irgendein Event (egal ob RENTAL, SHIFT oder PRESENCE), das sich mit diesem Datum auf 'Haus A' überschneidet?"*
Wenn Sev also für seine Workation das Haus A als "Anwesenheit" blockt, kann kein Gast es buchen. Man muss nicht drei verschiedene Tabellen (`Urlaub`, `Buchungen`, `Sperrzeiten`) abfragen!
+29
View File
@@ -0,0 +1,29 @@
# RoggioApp - Replikation & Hardcore-Datenschutz (Online vs. On-Premise)
## Die Anforderung
Zwei aktive Datenbanken (Cloud bei Hetzner + On-Premise auf dem Gelände).
**Das Ziel:** Sensible Daten (Ausweisnummern, IBANs, Kreditkarten) dürfen **niemals** in der Cloud-Datenbank liegen. Die Cloud hat nur Pointer (Referenzen) auf diese Daten. Die echte Entschlüsselung und Speicherung findet nur im Safe (On-Premise) statt.
## Die Lösungsarchitektur (Der Hybrid-Ansatz)
Wir können nicht einfach eine blinde PostgreSQL-Replikation (Master-Slave) einschalten. Wenn die Cloud der Master ist, hat sie alle Daten. Wenn wir das verhindern wollen, müssen wir die Applikations-Architektur anpassen.
### 1. Tokenisierung (Das Pointer-Konzept)
Die Cloud-Datenbank (Hetzner) speichert keine JSONB-Traits mit sensiblen Daten.
Anstatt: `traits: { iban: "DE1234...", passport: "X123" }`
Speichert die Cloud: `traits: { secure_vault_id: "vault_abc123" }`
### 2. Der Vault (On-Premise)
Auf dem Server im Gelände läuft ein isolierter "Vault-Service" (z.B. HashiCorp Vault oder eine eigene hochsichere Micro-Postgres-Instanz).
Dort liegt die Tabelle `Vault_Data` mit den Inhalten für `vault_abc123`.
### 3. Wie die API damit umgeht
* **Der Cloud-API-Server (Hetzner):** Handelt nur Termine, Namen (ohne Ausweise), Unit-Kapazitäten und Buchungs-Slots. Wenn ein Gast via WordPress bucht und seine IBAN eintippt, schickt die Cloud-API diese IBAN *direkt* durch einen verschlüsselten Tunnel (WireGuard) an den Vault On-Premise, holt sich eine generierte `vault_id` zurück und speichert *nur diese ID* in der Cloud-Datenbank.
* **Der On-Premise-API-Server (Das Kollektiv-Cockpit):** Wenn du zu Hause im Netzwerk die Buchung aufmachst, lädt das Frontend die Buchung (aus der Cloud-DB) PLUS die sensiblen Daten (aus dem lokalen Vault) und fügt sie in deinem Browser zusammen.
### 4. Was passiert bei Internet-Ausfall?
Wenn das Gelände offline ist, kann die Cloud immer noch fröhlich neue Buchungen über WordPress annehmen. Sie speichert die sensiblen Daten (z.B. Ausweisfotos) temporär hochverschlüsselt (asymmetrisch, z.B. mit PGP) zwischen. Sobald die On-Premise-Kiste wieder online geht, zieht sie sich die verschlüsselten Pakete, entschlüsselt sie mit ihrem privaten Offline-Key, speichert sie im Vault und sagt der Cloud: "Hab ich, du kannst das Original jetzt löschen."
## Claras Einschätzung
Das ist Enterprise-Level Security (Zero Trust für die Cloud).
Für Prisma bedeutet das: Wir trennen die sensiblen Traits strikt ab. Wir können das relativ elegant bauen, indem wir für sensible Traits eine eigene Prisma-Logik verwenden, die immer mit einem On-Prem-Service redet, statt sie ins normale Cloud-JSONB-Feld zu kippen.
+29
View File
@@ -0,0 +1,29 @@
# RoggioApp - Vault Implementierung (MVP Step 1)
## Die MVP-Strategie für den Datenschutz-Split
Wir bauen nicht sofort eine zweite physische Datenbank und einen VPN-Tunnel auf. Das würde uns am Anfang massiv ausbremsen. Wir simulieren die Architektur zuerst *logisch*, bevor wir sie *physisch* trennen.
### Schritt 1 (Jetzt): Die logische Trennung (Separate Table)
In unserer aktuellen Prisma-Datenbank (Hetzner/Cloud) legen wir eine **eigenständige Tabelle `Vault`** an.
* **Warum:** So zwingen wir unseren Backend-Code vom ersten Tag an dazu, so zu tun, als lägen die Daten auf einem anderen Server. Die Logik "Hole Token -> Lade Daten aus Vault" wird in Code gegossen.
* **Der Vorteil:** Wenn wir später den echten On-Premise-Server hinstellen, müssen wir den Backend-Code nicht mehr anfassen. Wir ändern nur die Datenbank-URL für die `Vault`-Tabelle auf deine lokale IP um.
### Schritt 2 (Später): Die physische Trennung
Sobald der On-Premise-Server steht, lösen wir die Tabelle `Vault` aus Prisma heraus und schieben sie in eine eigene, kleine SQLite/Postgres-Instanz auf deinem HomeAssistant-Server.
## Anpassung für Prisma (Mock-Vault)
```prisma
// ============================================================================
// DER VAULT (Logisch getrennt - Später On-Premise)
// ============================================================================
model VaultData {
id String @id @default(uuid())
entityType String @db.VarChar(50) // e.g. "Person", "Booking"
entityId String // Die ID der Person oder Buchung in der Cloud
// Die sensiblen Daten (verschlüsselt!)
secureData String @db.Text
createdAt DateTime @default(now())
}
```
+14
View File
@@ -0,0 +1,14 @@
# RoggioApp - Nächster Schritt: Das Seed/CRUD Script
## Das Ziel
Die graue Theorie der Graphen-Architektur (`Units`, `Traits`, `Events`) mit echtem Leben füllen.
Wir brauchen einen visuellen / greifbaren Beweis, dass unser Prisma-Modell einen komplexen Baum (Wohnung im 1. Stock mit Inventar) abbilden kann und dass wir die Flächen/Kapazitäten rekursiv abfragen können.
## Der Plan: Ein Node.js Playground-Script
Bevor wir ein riesiges Frontend bauen, schreiben wir ein simples `seed.ts` oder `playground.ts` Skript, das die Prisma-API nutzt.
### Was das Script tun soll:
1. **Baum aufbauen:** Es erstellt das Grundstück, ein Haus, den 1. Stock und eine "Wohnung Sonnenaufgang" mit Küche, Bad und Schlafzimmern inklusive Betten und Besteck (als Child-Units).
2. **Personen anlegen:** Es erstellt eine Test-Person (z.B. "Sev") und speichert die "Ausweisnummer" testweise in unserer neuen `VaultData`-Tabelle, um die Trennung zu simulieren.
3. **Ein Event (Buchung) triggern:** Es verknüpft Sev mit der "Wohnung Sonnenaufgang" für ein Datum.
4. **Die Graph-Abfrage (Der Test):** Das Script führt die "Magic-Query" aus, die beweist, dass unser Ansatz funktioniert: Es fragt die Datenbank nach der Wohnung und rechnet rekursiv die Flächen der untergeordneten Räume und die Schlafplätze der Betten zusammen.
+12
View File
@@ -0,0 +1,12 @@
# RoggioApp - Trennung von "Gehirn" und "Muskeln"
## Die Philosophie
Die OpenClaw-Instanz (`192.168.20.199`) ist Claras Safe-Space. Hier läuft ihr Gedächtnis, ihre Konfiguration und die interne Steuerung.
Projekte (wie der API-Server, Datenbanken) dürfen hier *nicht* ausgeführt werden.
## Zukünftiger Standard-Workflow
1. Code wird von Clara in `/home/clara/.openclaw/workspace/projects` (was ein SMB-Mount auf die Docker-VM ist) geschrieben.
2. Anstatt Code mit `npx tsx` auf Claras Instanz auszuführen, nutzt Clara *ausschließlich* SSH, um den Code auf der Docker-VM (`192.168.20.252`) auszuführen.
3. Der Nginx Proxy Manager leitet Traffic immer auf die Docker-VM weiter.
*Folge:* Wenn ein API-Prototyp eine Endlosschleife hat oder RAM frisst, stürzt die Docker-VM ab. Clara bleibt aber zu 100% online und kann den Fehler beheben.
+21
View File
@@ -0,0 +1,21 @@
# RoggioApp - Infrastruktur: Gitea & Node.js Docker-Setup
## Die Notwendigkeit
Projekte wie RoggioApp oder unser API-Server sollten nicht einfach per `nohup` oder in `tmux`-Sessions auf der Docker-VM laufen. Das ist fehleranfällig und schlecht wartbar.
Zudem fehlt uns eine Versionskontrolle (Git) auf dem Server, um Code-Änderungen sicher nachverfolgen zu können, ohne Abhängigkeiten zu externen Anbietern (wie GitHub/GitLab) zu schaffen.
## Die Lösung: Das Infra-Compose-File
Wir erweitern unsere Sandkiste (`192.168.20.252`) um ein sauberes Infrastruktur-Compose-Setup:
### 1. Gitea (Lokales GitHub)
* *Warum:* Gitea ist extrem leichtgewichtig (geschrieben in Go), verbraucht kaum RAM und gibt uns eine professionelle Git-Oberfläche (Issues, Pull Requests, Code-Hosting) direkt im lokalen Netzwerk.
* *Setup:* Ein Container in der Docker-VM. Wir erstellen einen NPM-Proxy (z.B. `git.h80.11112222.net`).
### 2. Node.js Production-Serving (PM2 im Docker)
* *Warum:* Statt Node.js "nackt" auf dem Host (VM) laufen zu lassen, packen wir die Node.js-Applikationen in eigene Docker-Container.
* *Wie:* Für Test-API-Server (wie `bookme`) schreiben wir kleine `Dockerfile`s, die den Code bauen und per Node starten. Diese Container hängen wir dann in das Netzwerk des Nginx Proxy Managers.
## Nächste Schritte
1. Eine `docker-compose.yml` für Gitea anlegen und starten.
2. Die Subdomain `git.h80.11112222.net` im Nginx Proxy Manager eintragen.
3. Das RoggioApp-Projekt als erstes Repo initialisieren.
+146
View File
@@ -0,0 +1,146 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
console.log('🧹 Lösche alte Daten für sauberen Testlauf...')
await prisma.event.deleteMany()
await prisma.groupMembership.deleteMany()
await prisma.group.deleteMany()
await prisma.person.deleteMany()
await prisma.unit.deleteMany()
await prisma.vaultData.deleteMany()
console.log('🏗️ Baue den Unit-Graphen (Grundstück -> Haus -> Wohnung -> Räume -> Inventar)...')
// 1. Grundstück
const campus = await prisma.unit.create({
data: {
name: 'Campus Hauptgelände',
traits: { type: 'campus' }
}
})
// 2. Haus A
const houseA = await prisma.unit.create({
data: {
name: 'Haus A',
parentId: campus.id,
traits: { type: 'building' }
}
})
// 3. Wohnung Sonnenaufgang (Rental Unit)
const apartment = await prisma.unit.create({
data: {
name: 'Wohnung Sonnenaufgang (1. Stock)',
parentId: houseA.id,
traits: {
type: 'apartment',
is_rentable: true,
base_price: 85.00
}
}
})
// 4. Räume der Wohnung
const kitchen = await prisma.unit.create({
data: { name: 'Küche', parentId: apartment.id, traits: { type: 'room', area_sqm: 12.0 } }
})
const bedroom1 = await prisma.unit.create({
data: { name: 'Schlafzimmer Groß', parentId: apartment.id, traits: { type: 'room', area_sqm: 18.5 } }
})
// 5. Inventar als Units (Sub-Units der Räume)
await prisma.unit.create({
data: { name: 'Doppelbett', parentId: bedroom1.id, traits: { type: 'inventory', inventory_category: 'bed', sleep_capacity: 2 } }
})
// 6. Test: Gasflaschen im Butanlager (außerhalb der Wohnung, direkt im Haus)
const storage = await prisma.unit.create({
data: { name: 'Butanlager', parentId: houseA.id, traits: { type: 'room', needs_maintenance: true } }
})
await prisma.unit.create({
data: { name: '5kg Butangas Vorrat', parentId: storage.id, traits: { type: 'stock', soll: 10, ist: 7, alert_limit: 3 } }
})
console.log('👤 Erstelle Person (Sev) und simuliere Vault...')
// Vault Data Simulation
const secureVaultEntry = await prisma.vaultData.create({
data: {
entityType: 'Person',
entityId: 'pending', // Wird nach Person-Erstellung aktualisiert
secureData: JSON.stringify({ passport: 'X123456789', iban: 'DE1234...' })
}
})
const sev = await prisma.person.create({
data: {
firstName: 'Sev',
lastName: 'Atzinger',
email: 'sev@example.com',
traits: {
pronouns: 'er/ihm',
vault_id: secureVaultEntry.id
}
}
})
await prisma.vaultData.update({ where: { id: secureVaultEntry.id }, data: { entityId: sev.id } })
console.log('📅 Simuliere Event: Sev bucht die Wohnung Sonnenaufgang...')
await prisma.event.create({
data: {
type: 'RENTAL',
status: 'CONFIRMED',
startTime: new Date('2026-08-14T15:00:00Z'),
endTime: new Date('2026-08-21T10:00:00Z'),
targetUnitId: apartment.id,
targetPersonId: sev.id,
payload: { guests: 2, total_price: 595.00 }
}
})
console.log('\n--- 🔍 TEST: DYNAMISCHE ABFRAGE DER WOHNUNG ---')
// Wir holen die Wohnung und generieren die Kapazität aus den Kindern (Tiefen-Abfrage)
const apartmentWithChildren = await prisma.unit.findUnique({
where: { id: apartment.id },
include: {
children: {
include: { children: true } // Wir holen Räume UND deren Inventar
}
}
})
let totalArea = 0;
let totalSleepCapacity = 0;
apartmentWithChildren?.children.forEach(room => {
if (room.traits && typeof room.traits === 'object' && 'area_sqm' in room.traits) {
totalArea += (room.traits as any).area_sqm;
}
// Check Inventar im Raum
room.children.forEach(inventory => {
if (inventory.traits && typeof inventory.traits === 'object' && 'sleep_capacity' in inventory.traits) {
totalSleepCapacity += (inventory.traits as any).sleep_capacity;
}
})
})
console.log(`Wohnung: ${apartmentWithChildren?.name}`)
console.log(`Gesamtfläche: ${totalArea} qm (Berechnet aus den Räumen)`)
console.log(`Schlafplätze: ${totalSleepCapacity} (Berechnet aus den Betten)`)
console.log('------------------------------------------------\n')
}
main()
.catch(e => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
+78
View File
@@ -0,0 +1,78 @@
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { PrismaClient } from '@prisma/client'
const app = new Hono()
const prisma = new PrismaClient()
app.get('/', (c) => {
return c.html(`
<html>
<head>
<title>BookMe - Roggio API</title>
<style>
body { font-family: monospace; background: #282a36; color: #f8f8f2; padding: 2rem; }
h1 { color: #50fa7b; }
.unit { border: 1px solid #6272a4; padding: 1rem; margin-bottom: 1rem; border-radius: 8px; }
</style>
</head>
<body>
<h1>BookMe API (RoggioApp MVP)</h1>
<p>Willkommen an der API-Schnittstelle. Test-Endpunkte:</p>
<ul>
<li><a href="/api/apartments" style="color: #ff79c6;">/api/apartments</a> - Zeigt alle mietbaren Einheiten (dynamisch berechnet)</li>
</ul>
</body>
</html>
`)
})
app.get('/api/apartments', async (c) => {
const units = await prisma.unit.findMany({
include: {
children: {
include: { children: true }
}
}
})
const apartments = units.filter(u => {
const traits = u.traits as any
return traits && traits.is_rentable === true
}).map(apartment => {
let totalArea = 0;
let totalSleepCapacity = 0;
apartment.children.forEach(room => {
const rTraits = room.traits as any;
if (rTraits?.area_sqm) totalArea += rTraits.area_sqm;
room.children.forEach(inv => {
const iTraits = inv.traits as any;
if (iTraits?.sleep_capacity) totalSleepCapacity += iTraits.sleep_capacity;
});
});
return {
id: apartment.id,
name: apartment.name,
base_price: (apartment.traits as any).base_price,
calculated_stats: {
area_sqm: totalArea,
sleep_capacity: totalSleepCapacity
},
rooms: apartment.children.map(r => r.name)
}
})
return c.json(apartments)
})
const port = 3000
console.log(`Server is running on port ${port}`)
serve({
fetch: app.fetch,
port,
hostname: '0.0.0.0'
})
Generated Vendored Executable
View File
Generated Vendored Executable
View File
Generated Vendored Executable
View File
Generated Vendored Executable
View File
Generated Vendored Executable
View File
Generated Vendored Executable
View File
Generated Vendored Executable
View File
+11
View File
@@ -0,0 +1,11 @@
"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.default = void 0;var _config = await jitiImport("@prisma/config");var _default = exports.default =
(0, _config.defineConfig)({
earlyAccess: true,
schema: {
url: 'postgresql://roggio:roggio_secret@192.168.20.252:5432/roggiodb'
},
migrations: {
url: 'postgresql://roggio:roggio_secret@192.168.20.252:5432/roggiodb'
}
}); /* v9-fa9df8f6b9f1b8a7 */
Generated Vendored Executable
+552
View File
@@ -0,0 +1,552 @@
{
"name": "roggioapp",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@hono/node-server": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.0.tgz",
"integrity": "sha512-n3GfHwwCvHCkGmOwKfxUPOlbfzuO64Sbc5XC4NGPIXxkuOnJrdgExdRKmHfF924r914WRJPT397GdqLvdYTeyQ==",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"hono": "^4"
}
},
"node_modules/@prisma/client": {
"version": "6.19.3",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz",
"integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==",
"hasInstallScript": true,
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/config": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.8.0.tgz",
"integrity": "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.3.4",
"deepmerge-ts": "7.1.5",
"effect": "3.20.0",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz",
"integrity": "sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz",
"integrity": "sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/fetch-engine": "6.4.1",
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d.tgz",
"integrity": "sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz",
"integrity": "sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz",
"integrity": "sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/c12": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.3.4.tgz",
"integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^5.0.0",
"confbox": "^0.2.4",
"defu": "^6.1.6",
"dotenv": "^17.3.1",
"exsolve": "^1.0.8",
"giget": "^3.2.0",
"jiti": "^2.6.1",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^2.1.0",
"pkg-types": "^2.3.0",
"rc9": "^3.0.1"
},
"peerDependencies": {
"magicast": "*"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/confbox": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
"integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
"dev": true,
"license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/defu": {
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"dev": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"dev": true,
"license": "MIT"
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/effect": {
"version": "3.20.0",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.20.0.tgz",
"integrity": "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/exsolve": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
"integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
},
"funding": {
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/giget": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz",
"integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==",
"dev": true,
"license": "MIT",
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/hono": {
"version": "4.12.15",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
"integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
"engines": {
"node": ">=16.9.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
"integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
"dev": true,
"license": "MIT"
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"dev": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/prisma": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz",
"integrity": "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/engines": "6.4.1",
"esbuild": ">=0.12 <1",
"esbuild-register": "3.6.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"optionalDependencies": {
"fsevents": "2.3.3"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-3.0.1.tgz",
"integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.6",
"destr": "^2.0.5"
}
},
"node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
}
}
}
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }
+1
View File
@@ -0,0 +1 @@
export * from "./default"
+356
View File
File diff suppressed because one or more lines are too long
+346
View File
@@ -0,0 +1,346 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('@prisma/client/runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 6.19.3
* Query Engine version: a9055b89e58b4b5bfb59600785423b1db3d0e75d
*/
Prisma.prismaVersion = {
client: "6.19.3",
engine: "a9055b89e58b4b5bfb59600785423b1db3d0e75d"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.UnitScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
parentId: 'parentId',
traits: 'traits',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.PersonScalarFieldEnum = {
id: 'id',
firstName: 'firstName',
lastName: 'lastName',
email: 'email',
phone: 'phone',
ssoId: 'ssoId',
traits: 'traits',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.GroupScalarFieldEnum = {
id: 'id',
name: 'name',
traits: 'traits',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.GroupMembershipScalarFieldEnum = {
id: 'id',
personId: 'personId',
groupId: 'groupId',
role: 'role'
};
exports.Prisma.EventScalarFieldEnum = {
id: 'id',
type: 'type',
status: 'status',
startTime: 'startTime',
endTime: 'endTime',
targetUnitId: 'targetUnitId',
targetGroupId: 'targetGroupId',
targetPersonId: 'targetPersonId',
payload: 'payload',
parentEventId: 'parentEventId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.CitiesScalarFieldEnum = {
id: 'id',
name: 'name',
state_id: 'state_id',
state_code: 'state_code',
country_id: 'country_id',
country_code: 'country_code',
type: 'type',
level: 'level',
parent_id: 'parent_id',
latitude: 'latitude',
longitude: 'longitude',
native: 'native',
population: 'population',
timezone: 'timezone',
translations: 'translations',
created_at: 'created_at',
updated_at: 'updated_at',
flag: 'flag',
wikiDataId: 'wikiDataId'
};
exports.Prisma.CountriesScalarFieldEnum = {
id: 'id',
name: 'name',
iso3: 'iso3',
numeric_code: 'numeric_code',
iso2: 'iso2',
phonecode: 'phonecode',
capital: 'capital',
currency: 'currency',
currency_name: 'currency_name',
currency_symbol: 'currency_symbol',
tld: 'tld',
native: 'native',
population: 'population',
gdp: 'gdp',
region: 'region',
region_id: 'region_id',
subregion: 'subregion',
subregion_id: 'subregion_id',
nationality: 'nationality',
area_sq_km: 'area_sq_km',
postal_code_format: 'postal_code_format',
postal_code_regex: 'postal_code_regex',
timezones: 'timezones',
translations: 'translations',
latitude: 'latitude',
longitude: 'longitude',
emoji: 'emoji',
emojiU: 'emojiU',
created_at: 'created_at',
updated_at: 'updated_at',
flag: 'flag',
wikiDataId: 'wikiDataId'
};
exports.Prisma.StatesScalarFieldEnum = {
id: 'id',
name: 'name',
country_id: 'country_id',
country_code: 'country_code',
fips_code: 'fips_code',
iso2: 'iso2',
iso3166_2: 'iso3166_2',
type: 'type',
level: 'level',
parent_id: 'parent_id',
native: 'native',
latitude: 'latitude',
longitude: 'longitude',
timezone: 'timezone',
translations: 'translations',
created_at: 'created_at',
updated_at: 'updated_at',
flag: 'flag',
wikiDataId: 'wikiDataId',
population: 'population'
};
exports.Prisma.AddressScalarFieldEnum = {
id: 'id',
street: 'street',
streetNo: 'streetNo',
postalCode: 'postalCode',
cityId: 'cityId',
stateId: 'stateId',
countryId: 'countryId',
rawCity: 'rawCity',
rawCountry: 'rawCountry',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.VaultDataScalarFieldEnum = {
id: 'id',
entityType: 'entityType',
entityId: 'entityId',
secureData: 'secureData',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.JsonNullValueInput = {
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
Unit: 'Unit',
Person: 'Person',
Group: 'Group',
GroupMembership: 'GroupMembership',
Event: 'Event',
cities: 'cities',
countries: 'countries',
states: 'states',
Address: 'Address',
VaultData: 'VaultData'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+19016
View File
File diff suppressed because it is too large Load Diff
+379
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
+183
View File
@@ -0,0 +1,183 @@
{
"name": "prisma-client-d8a198d17d4a3c96c5647b2d68e00f04d7d64ee8576c926d0e499bf11ed9b4a9",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "6.19.3",
"sideEffects": false,
"imports": {
"#wasm-engine-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}
File diff suppressed because one or more lines are too long
Binary file not shown.
Generated Vendored Executable
+199
View File
@@ -0,0 +1,199 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = "postgresql://roggio:roggio_secret@192.168.20.252:5432/roggiodb"
}
model Unit {
id String @id @default(uuid())
name String @db.VarChar(255)
description String?
parentId String?
traits Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parent Unit? @relation("UnitHierarchy", fields: [parentId], references: [id])
children Unit[] @relation("UnitHierarchy")
}
model Person {
id String @id @default(uuid())
firstName String @db.VarChar(100)
lastName String @db.VarChar(100)
email String? @unique @db.VarChar(255)
phone String? @db.VarChar(50)
ssoId String? @unique @db.VarChar(255)
traits Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
groupMemberships GroupMembership[]
}
model Group {
id String @id @default(uuid())
name String @db.VarChar(255)
traits Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
members GroupMembership[]
}
model GroupMembership {
id String @id @default(uuid())
personId String
groupId String
role String @db.VarChar(50)
group Group @relation(fields: [groupId], references: [id])
person Person @relation(fields: [personId], references: [id])
@@unique([personId, groupId, role])
}
model Event {
id String @id @default(uuid())
type String @db.VarChar(50)
status String @db.VarChar(50)
startTime DateTime
endTime DateTime?
targetUnitId String?
targetGroupId String?
targetPersonId String?
payload Json @default("{}")
parentEventId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model cities {
id BigInt @id @default(autoincrement())
name String @db.VarChar(255)
state_id BigInt
state_code String @db.VarChar(255)
country_id BigInt
country_code String @db.Char(2)
type String? @db.VarChar(191)
level Int?
parent_id BigInt?
latitude Decimal @db.Decimal(10, 8)
longitude Decimal @db.Decimal(11, 8)
native String? @db.VarChar(255)
population BigInt?
timezone String? @db.VarChar(255)
translations String?
created_at DateTime @default(dbgenerated("'2014-01-01 12:01:01'::timestamp without time zone")) @db.Timestamp(6)
updated_at DateTime @default(now()) @db.Timestamp(6)
flag Int @default(1) @db.SmallInt
wikiDataId String? @db.VarChar(255)
countries countries @relation(fields: [country_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
states states @relation(fields: [state_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@index([country_id])
@@index([state_id])
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model countries {
id BigInt @id @default(autoincrement())
name String @db.VarChar(100)
iso3 String? @db.Char(3)
numeric_code String? @db.Char(3)
iso2 String? @db.Char(2)
phonecode String? @db.VarChar(255)
capital String? @db.VarChar(255)
currency String? @db.VarChar(255)
currency_name String? @db.VarChar(255)
currency_symbol String? @db.VarChar(255)
tld String? @db.VarChar(255)
native String? @db.VarChar(255)
population BigInt?
gdp BigInt?
region String? @db.VarChar(255)
region_id BigInt?
subregion String? @db.VarChar(255)
subregion_id BigInt?
nationality String? @db.VarChar(255)
area_sq_km Float?
postal_code_format String? @db.VarChar(255)
postal_code_regex String? @db.VarChar(255)
timezones String?
translations String?
latitude Decimal? @db.Decimal(10, 8)
longitude Decimal? @db.Decimal(11, 8)
emoji String? @db.VarChar(191)
emojiU String? @db.VarChar(191)
created_at DateTime? @db.Timestamp(6)
updated_at DateTime @default(now()) @db.Timestamp(6)
flag Int @default(1) @db.SmallInt
wikiDataId String? @db.VarChar(255)
cities cities[]
states states[]
@@index([region_id])
@@index([subregion_id])
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model states {
id BigInt @id @default(autoincrement())
name String @db.VarChar(255)
country_id BigInt
country_code String @db.Char(2)
fips_code String? @db.VarChar(255)
iso2 String? @db.VarChar(255)
iso3166_2 String? @db.VarChar(10)
type String? @db.VarChar(191)
level Int?
parent_id BigInt?
native String? @db.VarChar(255)
latitude Decimal? @db.Decimal(10, 8)
longitude Decimal? @db.Decimal(11, 8)
timezone String? @db.VarChar(255)
translations String?
created_at DateTime? @db.Timestamp(6)
updated_at DateTime @default(now()) @db.Timestamp(6)
flag Int @default(1) @db.SmallInt
wikiDataId String? @db.VarChar(255)
population String? @db.VarChar(255)
cities cities[]
countries countries @relation(fields: [country_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@index([country_id])
}
// ============================================================================
// ADRESSEN (Shared Locations)
// ============================================================================
model Address {
id String @id @default(uuid())
street String @db.VarChar(255)
streetNo String? @db.VarChar(50)
postalCode String? @db.VarChar(50)
cityId Int?
stateId Int?
countryId Int?
rawCity String? @db.VarChar(255)
rawCountry String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============================================================================
// DER VAULT (Logisch getrennt - Später On-Premise)
// ============================================================================
model VaultData {
id String @id @default(uuid())
entityType String @db.VarChar(50)
entityId String
// Die sensiblen Daten (sollten auf App-Ebene verschlüsselt werden)
secureData String @db.Text
createdAt DateTime @default(now())
}
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm?module')
+5
View File
@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_engine_bg.wasm')
+1
View File
@@ -0,0 +1 @@
export * from "./default"
+363
View File
File diff suppressed because one or more lines are too long
Generated Vendored Executable
+3
View File
@@ -0,0 +1,3 @@
# esbuild
This is the Linux 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
Generated Vendored Executable
BIN
View File
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@esbuild/linux-x64",
"version": "0.27.7",
"description": "The Linux 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"linux"
],
"cpu": [
"x64"
]
}
Generated Vendored Executable
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 - present, Yusuke Wada and Hono contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Generated Vendored Executable
+396
View File
@@ -0,0 +1,396 @@
# Node.js Adapter for Hono
This adapter `@hono/node-server` allows you to run your Hono application on Node.js.
Initially, Hono wasn't designed for Node.js, but with this adapter, you can now use Hono on Node.js. It utilizes web standard APIs implemented in Node.js.
## Benchmarks
Hono is 4.1 times faster than Express.
Express:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 20803.37 1713.06 24910.85
Latency 6.01ms 5.21ms 451.37ms
HTTP codes:
1xx - 0, 2xx - 208131, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 5.75MB/s
```
Hono + `@hono/node-server`:
```txt
$ bombardier -d 10s --fasthttp http://localhost:3000/
Statistics Avg Stdev Max
Reqs/sec 85405.51 7250.65 102658.51
Latency 1.46ms 1.00ms 149.95ms
HTTP codes:
1xx - 0, 2xx - 854120, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 18.49MB/s
```
## Requirements
It works on Node.js versions greater than 20.x.
## Installation
You can install it from the npm registry with `npm` command:
```sh
npm install @hono/node-server
```
Or use `yarn`:
```sh
yarn add @hono/node-server
```
## Usage
Just import `@hono/node-server` at the top and write the code as usual.
The same code that runs on Cloudflare Workers, Deno, and Bun will work.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hono meets Node.js'))
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`) // Listening on http://localhost:3000
})
```
## WebSocket
You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`.
To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below.
```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'
import { Hono } from 'hono'
const app = new Hono()
app.get(
'/ws',
upgradeWebSocket(() => ({
onMessage(event, ws) {
ws.send(event.data)
},
}))
)
const wss = new WebSocketServer({ noServer: true }) // important to create with `noServer: true`
serve({
fetch: app.fetch,
websocket: { server: wss },
})
```
For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`.
```sh
ts-node ./index.ts
```
Open `http://localhost:3000` with your browser.
## Options
### `port`
```ts
serve({
fetch: app.fetch,
port: 8787, // Port number, default is 3000
})
```
### `createServer`
```ts
import { createServer } from 'node:https'
import fs from 'node:fs'
//...
serve({
fetch: app.fetch,
createServer: createServer,
serverOptions: {
key: fs.readFileSync('test/fixtures/keys/agent1-key.pem'),
cert: fs.readFileSync('test/fixtures/keys/agent1-cert.pem'),
},
})
```
### `overrideGlobalObjects`
The default value is `true`. The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance. If you don't want to do that, set `false`.
```ts
serve({
fetch: app.fetch,
overrideGlobalObjects: false,
})
```
### `autoCleanupIncoming`
The default value is `true`. The Node.js Adapter automatically cleans up (explicitly call `destroy()` method) if application is not finished to consume the incoming request. If you don't want to do that, set `false`.
If the application accepts connections from arbitrary clients, this cleanup must be done otherwise incomplete requests from clients may cause the application to stop responding. If your application only accepts connections from trusted clients, such as in a reverse proxy environment and there is no process that returns a response without reading the body of the POST request all the way through, you can improve performance by setting it to `false`.
```ts
serve({
fetch: app.fetch,
autoCleanupIncoming: false,
})
```
### `websocket`
provide a websocket server to enable websocket support.
```ts
import { serve, upgradeWebSocket } from '@hono/node-server'
import { WebSocketServer } from 'ws'
// ...
const wss = new WebSocketServer({ noServer: true })
serve({
fetch: app.fetch,
websocket: { server: wss },
})
```
## Middleware
Most built-in middleware also works with Node.js.
Read [the documentation](https://hono.dev/middleware/builtin/basic-auth) and use the Middleware of your liking.
```ts
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { prettyJSON } from 'hono/pretty-json'
const app = new Hono()
app.get('*', prettyJSON())
app.get('/', (c) => c.json({ 'Hono meets': 'Node.js' }))
serve(app)
```
## Serve Static Middleware
Use Serve Static Middleware that has been created for Node.js.
```ts
import { serveStatic } from '@hono/node-server/serve-static'
//...
app.use('/static/*', serveStatic({ root: './' }))
```
If using a relative path, `root` will be relative to the current working directory from which the app was started.
This can cause confusion when running your application locally.
Imagine your project structure is:
```
my-hono-project/
src/
index.ts
static/
index.html
```
Typically, you would run your app from the project's root directory (`my-hono-project`),
so you would need the following code to serve the `static` folder:
```ts
app.use('/static/*', serveStatic({ root: './static' }))
```
Notice that `root` here is not relative to `src/index.ts`, rather to `my-hono-project`.
### Options
#### `rewriteRequestPath`
If you want to serve files in `./.foojs` with the request path `/__foo/*`, you can write like the following.
```ts
app.use(
'/__foo/*',
serveStatic({
root: './.foojs/',
rewriteRequestPath: (path: string) => path.replace(/^\/__foo/, ''),
})
)
```
#### `onFound`
You can specify handling when the requested file is found with `onFound`.
```ts
app.use(
'/static/*',
serveStatic({
// ...
onFound: (_path, c) => {
c.header('Cache-Control', `public, immutable, max-age=31536000`)
},
})
)
```
#### `onNotFound`
The `onNotFound` is useful for debugging. You can write a handle for when a file is not found.
```ts
app.use(
'/static/*',
serveStatic({
root: './non-existent-dir',
onNotFound: (path, c) => {
console.log(`${path} is not found, request to ${c.req.path}`)
},
})
)
```
#### `precompressed`
The `precompressed` option checks if files with extensions like `.br` or `.gz` are available and serves them based on the `Accept-Encoding` header. It prioritizes Brotli, then Zstd, and Gzip. If none are available, it serves the original file.
```ts
app.use(
'/static/*',
serveStatic({
precompressed: true,
})
)
```
## ConnInfo Helper
You can use the [ConnInfo Helper](https://hono.dev/docs/helpers/conninfo) by importing `getConnInfo` from `@hono/node-server/conninfo`.
```ts
import { getConnInfo } from '@hono/node-server/conninfo'
app.get('/', (c) => {
const info = getConnInfo(c) // info is `ConnInfo`
return c.text(`Your remote address is ${info.remote.address}`)
})
```
## Accessing Node.js API
You can access the Node.js API from `c.env` in Node.js. For example, if you want to specify a type, you can write the following.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
return c.json({
remoteAddress: c.env.incoming.socket.remoteAddress,
})
})
serve(app)
```
The APIs that you can get from `c.env` are as follows.
```ts
type HttpBindings = {
incoming: IncomingMessage
outgoing: ServerResponse
}
type Http2Bindings = {
incoming: Http2ServerRequest
outgoing: Http2ServerResponse
}
```
## Direct response from Node.js API
You can directly respond to the client from the Node.js API.
In that case, the response from Hono should be ignored, so return `RESPONSE_ALREADY_SENT`.
> [!NOTE]
> This feature can be used when migrating existing Node.js applications to Hono, but we recommend using Hono's API for new applications.
```ts
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'
import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response'
import { Hono } from 'hono'
const app = new Hono<{ Bindings: HttpBindings }>()
app.get('/', (c) => {
const { outgoing } = c.env
outgoing.writeHead(200, { 'Content-Type': 'text/plain' })
outgoing.end('Hello World\n')
return RESPONSE_ALREADY_SENT
})
serve(app)
```
## Listen to a UNIX domain socket
You can configure the HTTP server to listen to a UNIX domain socket instead of a TCP port.
```ts
import { createAdaptorServer } from '@hono/node-server'
// ...
const socketPath = '/tmp/example.sock'
const server = createAdaptorServer(app)
server.listen(socketPath, () => {
console.log(`Listening on ${socketPath}`)
})
```
## Related projects
- Hono - <https://hono.dev>
- Hono GitHub repository - <https://github.com/honojs/hono>
## Authors
- Yusuke Wada <https://github.com/yusukebe>
- Taku Amano <https://github.com/usualoma>
## License
MIT
+22
View File
@@ -0,0 +1,22 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
//#region src/conninfo.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
const getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return { remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
} };
};
//#endregion
exports.getConnInfo = getConnInfo;
+11
View File
@@ -0,0 +1,11 @@
import { GetConnInfo } from "hono/conninfo";
//#region src/conninfo.d.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
//#endregion
export { getConnInfo };
+11
View File
@@ -0,0 +1,11 @@
import { GetConnInfo } from "hono/conninfo";
//#region src/conninfo.d.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
declare const getConnInfo: GetConnInfo;
//#endregion
export { getConnInfo };
+20
View File
@@ -0,0 +1,20 @@
//#region src/conninfo.ts
/**
* ConnInfo Helper for Node.js
* @param c Context
* @returns ConnInfo
*/
const getConnInfo = (c) => {
const bindings = c.env.server ? c.env.server : c.env;
const address = bindings.incoming.socket.remoteAddress;
const port = bindings.incoming.socket.remotePort;
const family = bindings.incoming.socket.remoteFamily;
return { remote: {
address,
port,
addressType: family === "IPv4" ? "IPv4" : family === "IPv6" ? "IPv6" : void 0
} };
};
//#endregion
export { getConnInfo };
+5
View File
@@ -0,0 +1,5 @@
//#region src/utils/response/constants.ts
const X_ALREADY_SENT = "x-hono-already-sent";
//#endregion
export { X_ALREADY_SENT as t };
+11
View File
@@ -0,0 +1,11 @@
//#region src/utils/response/constants.ts
const X_ALREADY_SENT = "x-hono-already-sent";
//#endregion
Object.defineProperty(exports, 'X_ALREADY_SENT', {
enumerable: true,
get: function () {
return X_ALREADY_SENT;
}
});
+1006
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
import { AddressInfo } from "node:net";
import { WebSocket, WebSocketServer } from "ws";
import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http";
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2";
import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https";
import { UpgradeWebSocket } from "hono/ws";
//#region src/types.d.ts
type HttpBindings = {
incoming: IncomingMessage;
outgoing: ServerResponse;
};
type Http2Bindings = {
incoming: Http2ServerRequest;
outgoing: Http2ServerResponse;
};
type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise<unknown> | unknown;
type ServerType = Server | Http2Server | Http2SecureServer;
type createHttpOptions = {
serverOptions?: ServerOptions;
createServer?: typeof createServer;
};
type createHttpsOptions = {
serverOptions?: ServerOptions$2;
createServer?: typeof createServer$2;
};
type createHttp2Options = {
serverOptions?: ServerOptions$1;
createServer?: typeof createServer$1;
};
type createSecureHttp2Options = {
serverOptions?: SecureServerOptions;
createServer?: typeof createSecureServer;
};
type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options;
type Options = {
fetch: FetchCallback;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
port?: number;
hostname?: string;
websocket?: {
server: WebSocketServer;
};
} & ServerOptions$3;
type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
//#endregion
//#region src/server.d.ts
declare const createAdaptorServer: (options: Options) => ServerType;
declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType;
//#endregion
//#region src/websocket.d.ts
type UpgradeWebSocketOptions = {
onError: (err: unknown) => void;
};
declare const upgradeWebSocket: UpgradeWebSocket<WebSocket, UpgradeWebSocketOptions>;
//#endregion
//#region src/listener.d.ts
declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
hostname?: string;
errorHandler?: CustomErrorHandler;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise<void>;
//#endregion
//#region src/error.d.ts
declare class RequestError extends Error {
constructor(message: string, options?: {
cause?: unknown;
});
}
//#endregion
export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, createAdaptorServer, getRequestListener, serve, upgradeWebSocket };
+73
View File
@@ -0,0 +1,73 @@
import { IncomingMessage, Server, ServerOptions, ServerResponse, createServer } from "node:http";
import { Http2SecureServer, Http2Server, Http2ServerRequest, Http2ServerResponse, SecureServerOptions, ServerOptions as ServerOptions$1, createSecureServer, createServer as createServer$1 } from "node:http2";
import { UpgradeWebSocket } from "hono/ws";
import { AddressInfo } from "node:net";
import { WebSocket, WebSocketServer } from "ws";
import { ServerOptions as ServerOptions$2, createServer as createServer$2 } from "node:https";
//#region src/types.d.ts
type HttpBindings = {
incoming: IncomingMessage;
outgoing: ServerResponse;
};
type Http2Bindings = {
incoming: Http2ServerRequest;
outgoing: Http2ServerResponse;
};
type FetchCallback = (request: Request, env: HttpBindings | Http2Bindings) => Promise<unknown> | unknown;
type ServerType = Server | Http2Server | Http2SecureServer;
type createHttpOptions = {
serverOptions?: ServerOptions;
createServer?: typeof createServer;
};
type createHttpsOptions = {
serverOptions?: ServerOptions$2;
createServer?: typeof createServer$2;
};
type createHttp2Options = {
serverOptions?: ServerOptions$1;
createServer?: typeof createServer$1;
};
type createSecureHttp2Options = {
serverOptions?: SecureServerOptions;
createServer?: typeof createSecureServer;
};
type ServerOptions$3 = createHttpOptions | createHttpsOptions | createHttp2Options | createSecureHttp2Options;
type Options = {
fetch: FetchCallback;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
port?: number;
hostname?: string;
websocket?: {
server: WebSocketServer;
};
} & ServerOptions$3;
type CustomErrorHandler = (err: unknown) => void | Response | Promise<void | Response>;
//#endregion
//#region src/server.d.ts
declare const createAdaptorServer: (options: Options) => ServerType;
declare const serve: (options: Options, listeningListener?: (info: AddressInfo) => void) => ServerType;
//#endregion
//#region src/websocket.d.ts
type UpgradeWebSocketOptions = {
onError: (err: unknown) => void;
};
declare const upgradeWebSocket: UpgradeWebSocket<WebSocket, UpgradeWebSocketOptions>;
//#endregion
//#region src/listener.d.ts
declare const getRequestListener: (fetchCallback: FetchCallback, options?: {
hostname?: string;
errorHandler?: CustomErrorHandler;
overrideGlobalObjects?: boolean;
autoCleanupIncoming?: boolean;
}) => (incoming: IncomingMessage | Http2ServerRequest, outgoing: ServerResponse | Http2ServerResponse) => Promise<void>;
//#endregion
//#region src/error.d.ts
declare class RequestError extends Error {
constructor(message: string, options?: {
cause?: unknown;
});
}
//#endregion
export { type Http2Bindings, type HttpBindings, RequestError, type ServerType, createAdaptorServer, getRequestListener, serve, upgradeWebSocket };
+1001
View File
File diff suppressed because it is too large Load Diff
+135
View File
@@ -0,0 +1,135 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
let node_stream = require("node:stream");
let hono_utils_mime = require("hono/utils/mime");
let node_fs = require("node:fs");
let node_path = require("node:path");
let node_process = require("node:process");
//#region src/serve-static.ts
const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
const ENCODINGS = {
br: ".br",
zstd: ".zst",
gzip: ".gz"
};
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
const pr54206Applied = () => {
const [major, minor] = node_process.versions.node.split(".").map((component) => parseInt(component));
return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
};
const useReadableToWeb = pr54206Applied();
const createStreamBody = (stream) => {
if (useReadableToWeb) return node_stream.Readable.toWeb(stream);
return new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("error", (err) => {
controller.error(err);
});
stream.on("end", () => {
controller.close();
});
},
cancel() {
stream.destroy();
}
});
};
const getStats = (path) => {
let stats;
try {
stats = (0, node_fs.statSync)(path);
} catch {}
return stats;
};
const tryDecode = (str, decoder) => {
try {
return decoder(str);
} catch {
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
try {
return decoder(match);
} catch {
return match;
}
});
}
};
const tryDecodeURI = (str) => tryDecode(str, decodeURI);
const serveStatic = (options = { root: "" }) => {
const root = options.root || "";
const optionPath = options.path;
if (root !== "" && !(0, node_fs.existsSync)(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
return async (c, next) => {
if (c.finalized) return next();
let filename;
if (optionPath) filename = optionPath;
else try {
filename = tryDecodeURI(c.req.path);
if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}/.test(filename)) throw new Error();
} catch {
await options.onNotFound?.(c.req.path, c);
return next();
}
let path = (0, node_path.join)(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
let stats = getStats(path);
if (stats && stats.isDirectory()) {
const indexFile = options.index ?? "index.html";
path = (0, node_path.join)(path, indexFile);
stats = getStats(path);
}
if (!stats) {
await options.onNotFound?.(path, c);
return next();
}
const mimeType = (0, hono_utils_mime.getMimeType)(path);
c.header("Content-Type", mimeType || "application/octet-stream");
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) continue;
const precompressedStats = getStats(path + ENCODINGS[encoding]);
if (precompressedStats) {
c.header("Content-Encoding", encoding);
c.header("Vary", "Accept-Encoding", { append: true });
stats = precompressedStats;
path = path + ENCODINGS[encoding];
break;
}
}
}
let result;
const size = stats.size;
const range = c.req.header("range") || "";
if (c.req.method == "HEAD" || c.req.method == "OPTIONS") {
c.header("Content-Length", size.toString());
c.status(200);
result = c.body(null);
} else if (!range) {
c.header("Content-Length", size.toString());
result = c.body(createStreamBody((0, node_fs.createReadStream)(path)), 200);
} else {
c.header("Accept-Ranges", "bytes");
c.header("Date", stats.birthtime.toUTCString());
const parts = range.replace(/bytes=/, "").split("-", 2);
const start = parseInt(parts[0], 10) || 0;
let end = parseInt(parts[1], 10) || size - 1;
if (size < end - start + 1) end = size - 1;
const chunksize = end - start + 1;
const stream = (0, node_fs.createReadStream)(path, {
start,
end
});
c.header("Content-Length", chunksize.toString());
c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
result = c.body(createStreamBody(stream), 206);
}
await options.onFound?.(path, c);
return result;
};
};
//#endregion
exports.serveStatic = serveStatic;
+18
View File
@@ -0,0 +1,18 @@
import { Context, Env, MiddlewareHandler } from "hono";
//#region src/serve-static.d.ts
type ServeStaticOptions<E extends Env = Env> = {
/**
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
*/
root?: string;
path?: string;
index?: string;
precompressed?: boolean;
rewriteRequestPath?: (path: string, c: Context<E>) => string;
onFound?: (path: string, c: Context<E>) => void | Promise<void>;
onNotFound?: (path: string, c: Context<E>) => void | Promise<void>;
};
declare const serveStatic: <E extends Env = any>(options?: ServeStaticOptions<E>) => MiddlewareHandler<E>;
//#endregion
export { ServeStaticOptions, serveStatic };
+18
View File
@@ -0,0 +1,18 @@
import { Context, Env, MiddlewareHandler } from "hono";
//#region src/serve-static.d.ts
type ServeStaticOptions<E extends Env = Env> = {
/**
* Root path, relative to current working directory from which the app was started. Absolute paths are not supported.
*/
root?: string;
path?: string;
index?: string;
precompressed?: boolean;
rewriteRequestPath?: (path: string, c: Context<E>) => string;
onFound?: (path: string, c: Context<E>) => void | Promise<void>;
onNotFound?: (path: string, c: Context<E>) => void | Promise<void>;
};
declare const serveStatic: <E extends Env = any>(options?: ServeStaticOptions<E>) => MiddlewareHandler<E>;
//#endregion
export { ServeStaticOptions, serveStatic };
+134
View File
@@ -0,0 +1,134 @@
import { Readable } from "node:stream";
import { getMimeType } from "hono/utils/mime";
import { createReadStream, existsSync, statSync } from "node:fs";
import { join } from "node:path";
import { versions } from "node:process";
//#region src/serve-static.ts
const COMPRESSIBLE_CONTENT_TYPE_REGEX = /^\s*(?:text\/[^;\s]+|application\/(?:javascript|json|xml|xml-dtd|ecmascript|dart|postscript|rtf|tar|toml|vnd\.dart|vnd\.ms-fontobject|vnd\.ms-opentype|wasm|x-httpd-php|x-javascript|x-ns-proxy-autoconfig|x-sh|x-tar|x-virtualbox-hdd|x-virtualbox-ova|x-virtualbox-ovf|x-virtualbox-vbox|x-virtualbox-vdi|x-virtualbox-vhd|x-virtualbox-vmdk|x-www-form-urlencoded)|font\/(?:otf|ttf)|image\/(?:bmp|vnd\.adobe\.photoshop|vnd\.microsoft\.icon|vnd\.ms-dds|x-icon|x-ms-bmp)|message\/rfc822|model\/gltf-binary|x-shader\/x-fragment|x-shader\/x-vertex|[^;\s]+?\+(?:json|text|xml|yaml))(?:[;\s]|$)/i;
const ENCODINGS = {
br: ".br",
zstd: ".zst",
gzip: ".gz"
};
const ENCODINGS_ORDERED_KEYS = Object.keys(ENCODINGS);
const pr54206Applied = () => {
const [major, minor] = versions.node.split(".").map((component) => parseInt(component));
return major >= 23 || major === 22 && minor >= 7 || major === 20 && minor >= 18;
};
const useReadableToWeb = pr54206Applied();
const createStreamBody = (stream) => {
if (useReadableToWeb) return Readable.toWeb(stream);
return new ReadableStream({
start(controller) {
stream.on("data", (chunk) => {
controller.enqueue(chunk);
});
stream.on("error", (err) => {
controller.error(err);
});
stream.on("end", () => {
controller.close();
});
},
cancel() {
stream.destroy();
}
});
};
const getStats = (path) => {
let stats;
try {
stats = statSync(path);
} catch {}
return stats;
};
const tryDecode = (str, decoder) => {
try {
return decoder(str);
} catch {
return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
try {
return decoder(match);
} catch {
return match;
}
});
}
};
const tryDecodeURI = (str) => tryDecode(str, decodeURI);
const serveStatic = (options = { root: "" }) => {
const root = options.root || "";
const optionPath = options.path;
if (root !== "" && !existsSync(root)) console.error(`serveStatic: root path '${root}' is not found, are you sure it's correct?`);
return async (c, next) => {
if (c.finalized) return next();
let filename;
if (optionPath) filename = optionPath;
else try {
filename = tryDecodeURI(c.req.path);
if (/(?:^|[\/\\])\.{1,2}(?:$|[\/\\])|[\/\\]{2,}/.test(filename)) throw new Error();
} catch {
await options.onNotFound?.(c.req.path, c);
return next();
}
let path = join(root, !optionPath && options.rewriteRequestPath ? options.rewriteRequestPath(filename, c) : filename);
let stats = getStats(path);
if (stats && stats.isDirectory()) {
const indexFile = options.index ?? "index.html";
path = join(path, indexFile);
stats = getStats(path);
}
if (!stats) {
await options.onNotFound?.(path, c);
return next();
}
const mimeType = getMimeType(path);
c.header("Content-Type", mimeType || "application/octet-stream");
if (options.precompressed && (!mimeType || COMPRESSIBLE_CONTENT_TYPE_REGEX.test(mimeType))) {
const acceptEncodingSet = new Set(c.req.header("Accept-Encoding")?.split(",").map((encoding) => encoding.trim()));
for (const encoding of ENCODINGS_ORDERED_KEYS) {
if (!acceptEncodingSet.has(encoding)) continue;
const precompressedStats = getStats(path + ENCODINGS[encoding]);
if (precompressedStats) {
c.header("Content-Encoding", encoding);
c.header("Vary", "Accept-Encoding", { append: true });
stats = precompressedStats;
path = path + ENCODINGS[encoding];
break;
}
}
}
let result;
const size = stats.size;
const range = c.req.header("range") || "";
if (c.req.method == "HEAD" || c.req.method == "OPTIONS") {
c.header("Content-Length", size.toString());
c.status(200);
result = c.body(null);
} else if (!range) {
c.header("Content-Length", size.toString());
result = c.body(createStreamBody(createReadStream(path)), 200);
} else {
c.header("Accept-Ranges", "bytes");
c.header("Date", stats.birthtime.toUTCString());
const parts = range.replace(/bytes=/, "").split("-", 2);
const start = parseInt(parts[0], 10) || 0;
let end = parseInt(parts[1], 10) || size - 1;
if (size < end - start + 1) end = size - 1;
const chunksize = end - start + 1;
const stream = createReadStream(path, {
start,
end
});
c.header("Content-Length", chunksize.toString());
c.header("Content-Range", `bytes ${start}-${end}/${stats.size}`);
result = c.body(createStreamBody(stream), 206);
}
await options.onFound?.(path, c);
return result;
};
};
//#endregion
export { serveStatic };
+8
View File
@@ -0,0 +1,8 @@
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const require_constants = require('../constants-BXAKTxRC.cjs');
//#region src/utils/response.ts
const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [require_constants.X_ALREADY_SENT]: "true" } });
//#endregion
exports.RESPONSE_ALREADY_SENT = RESPONSE_ALREADY_SENT;
+4
View File
@@ -0,0 +1,4 @@
//#region src/utils/response.d.ts
declare const RESPONSE_ALREADY_SENT: Response;
//#endregion
export { RESPONSE_ALREADY_SENT };
+4
View File
@@ -0,0 +1,4 @@
//#region src/utils/response.d.ts
declare const RESPONSE_ALREADY_SENT: Response;
//#endregion
export { RESPONSE_ALREADY_SENT };
+7
View File
@@ -0,0 +1,7 @@
import { t as X_ALREADY_SENT } from "../constants-BLSFu_RU.mjs";
//#region src/utils/response.ts
const RESPONSE_ALREADY_SENT = new Response(null, { headers: { [X_ALREADY_SENT]: "true" } });
//#endregion
export { RESPONSE_ALREADY_SENT };
Generated Vendored Executable
+116
View File
@@ -0,0 +1,116 @@
{
"name": "@hono/node-server",
"version": "2.0.0",
"description": "Node.js Adapter for Hono",
"main": "dist/index.mjs",
"type": "module",
"types": "dist/index.d.mts",
"files": [
"dist"
],
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./serve-static": {
"import": {
"types": "./dist/serve-static.d.mts",
"default": "./dist/serve-static.mjs"
},
"require": {
"types": "./dist/serve-static.d.cts",
"default": "./dist/serve-static.cjs"
}
},
"./utils/*": {
"import": {
"types": "./dist/utils/*.d.mts",
"default": "./dist/utils/*.mjs"
},
"require": {
"types": "./dist/utils/*.d.cts",
"default": "./dist/utils/*.cjs"
}
},
"./conninfo": {
"import": {
"types": "./dist/conninfo.d.mts",
"default": "./dist/conninfo.mjs"
},
"require": {
"types": "./dist/conninfo.d.cts",
"default": "./dist/conninfo.cjs"
}
}
},
"typesVersions": {
"*": {
".": [
"./dist/index.d.mts"
],
"serve-static": [
"./dist/serve-static.d.mts"
],
"utils/*": [
"./dist/utils/*.d.mts"
],
"conninfo": [
"./dist/conninfo.d.mts"
]
}
},
"scripts": {
"test": "vitest",
"build": "tsdown --external hono",
"watch": "tsdown --watch",
"postbuild": "publint",
"prerelease": "bun run build && bun run test --run",
"release": "np",
"lint": "eslint src test",
"lint:fix": "eslint src test --fix",
"format": "prettier --check \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\"",
"format:fix": "prettier --write \"src/**/*.{js,ts}\" \"test/**/*.{js,ts}\""
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/honojs/node-server.git"
},
"homepage": "https://github.com/honojs/node-server",
"author": "Yusuke Wada <yusuke@kamawada.com> (https://github.com/yusukebe)",
"publishConfig": {
"registry": "https://registry.npmjs.org",
"access": "public"
},
"engines": {
"node": ">=20"
},
"devDependencies": {
"@hono/eslint-config": "^1.0.1",
"@types/node": "^20.10.0",
"@types/supertest": "^2.0.12",
"@types/ws": "^8.18.1",
"@whatwg-node/fetch": "^0.9.14",
"eslint": "^9.10.0",
"hono": "^4.12.8",
"np": "^7.7.0",
"prettier": "^3.2.4",
"publint": "^0.3.18",
"supertest": "^6.3.3",
"tsdown": "^0.20.3",
"typescript": "^5.3.2",
"vitest": "^4.0.18",
"ws": "^8.19.0"
},
"peerDependencies": {
"hono": "^4"
},
"packageManager": "bun@1.2.20"
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+27
View File
@@ -0,0 +1,27 @@
# Prisma Client &middot; [![npm version](https://img.shields.io/npm/v/@prisma/client.svg?style=flat)](https://www.npmjs.com/package/@prisma/client) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/prisma/prisma/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/937751382725886062?label=Discord)](https://pris.ly/discord)
Prisma Client JS is an **auto-generated query builder** that enables **type-safe** database access and **reduces boilerplate**. You can use it as an alternative to traditional ORMs such as Sequelize, TypeORM or SQL query builders like knex.js.
It is part of the [Prisma](https://www.prisma.io/) ecosystem. Prisma provides database tools for data access, declarative data modeling, schema migrations and visual data management. Learn more in the main [`prisma`](https://github.com/prisma/prisma/) repository or read the [documentation](https://www.prisma.io/docs/).
## Getting started
Follow one of these guides to get started with Prisma Client JS:
- [Quickstart](https://www.prisma.io/docs/getting-started/quickstart) (5 min)
- [Set up a new project with Prisma (SQL migrations)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-sql) (15 min)
- [Set up a new project with Prisma (Prisma Migrate)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-prisma-migrate) (15 min)
- [Add Prisma to an existing project](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project) (15 min)
Alternatively you can explore the ready-to-run [examples](https://github.com/prisma/prisma-examples/) (REST, GraphQL, gRPC, plain JavaScript and TypeScript demos, ...) or watch the [demo videos](https://www.youtube.com/watch?v=0RhtQgIs-TE&list=PLn2e1F9Rfr6k9PnR_figWOcSHgc_erDr5&index=1) (1-2 min per video).
## Contributing
Refer to our [contribution guidelines](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) and [Code of Conduct for contributors](https://github.com/prisma/prisma/blob/main/CODE_OF_CONDUCT.md).
## Tests Status
- Prisma Tests Status:
[![CI](https://github.com/prisma/prisma/actions/workflows/test.yml/badge.svg)](https://github.com/prisma/prisma/actions/workflows/test.yml)
- Ecosystem Tests Status:
[![Actions Status](https://github.com/prisma/ecosystem-tests/workflows/test/badge.svg)](https://github.com/prisma/ecosystem-tests/actions)
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/default'),
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/edge'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/edge'),
}
+1
View File
@@ -0,0 +1 @@
export * from './scripts/default-index'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('./scripts/default-index'),
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
const prisma = require('.prisma/client/index-browser')
module.exports = prisma
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/default'),
}
+327
View File
@@ -0,0 +1,327 @@
{
"name": "@prisma/client",
"version": "6.19.3",
"description": "Prisma Client is an auto-generated, type-safe and modern JavaScript/TypeScript ORM for Node.js that's tailored to your data. Supports PostgreSQL, CockroachDB, MySQL, MariaDB, SQL Server, SQLite & MongoDB databases.",
"keywords": [
"ORM",
"Prisma",
"prisma2",
"Prisma Client",
"client",
"query",
"query-builder",
"database",
"db",
"JavaScript",
"JS",
"TypeScript",
"TS",
"SQL",
"SQLite",
"pg",
"Postgres",
"PostgreSQL",
"CockroachDB",
"MySQL",
"MariaDB",
"MSSQL",
"SQL Server",
"SQLServer",
"MongoDB",
"react-native"
],
"main": "default.js",
"types": "default.d.ts",
"browser": "index-browser.js",
"exports": {
"./package.json": "./package.json",
".": {
"require": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"import": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"default": "./default.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.mjs",
"default": "./wasm.mjs"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.mjs",
"default": "./runtime/library.mjs"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.mjs",
"default": "./runtime/binary.mjs"
},
"./runtime/wasm-engine-edge": {
"types": "./runtime/wasm-engine-edge.d.ts",
"require": "./runtime/wasm-engine-edge.js",
"import": "./runtime/wasm-engine-edge.mjs",
"default": "./runtime/wasm-engine-edge.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/edge": {
"types": "./runtime/edge.d.ts",
"require": "./runtime/edge.js",
"import": "./runtime/edge-esm.js",
"default": "./runtime/edge-esm.js"
},
"./runtime/react-native": {
"types": "./runtime/react-native.d.ts",
"require": "./runtime/react-native.js",
"import": "./runtime/react-native.js",
"default": "./runtime/react-native.js"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"homepage": "https://www.prisma.io",
"repository": {
"type": "git",
"url": "https://github.com/prisma/prisma.git",
"directory": "packages/client"
},
"author": "Tim Suchanek <suchanek@prisma.io>",
"bugs": "https://github.com/prisma/prisma/issues",
"files": [
"README.md",
"runtime",
"scripts",
"generator-build",
"edge.js",
"edge.d.ts",
"wasm.js",
"wasm.d.ts",
"index.js",
"index.d.ts",
"react-native.js",
"react-native.d.ts",
"default.js",
"default.d.ts",
"index-browser.js",
"extension.js",
"extension.d.ts",
"sql.d.ts",
"sql.js",
"sql.mjs"
],
"devDependencies": {
"@cloudflare/workers-types": "^4.20250408.0",
"@codspeed/benchmark.js-plugin": "4.0.0",
"@faker-js/faker": "9.6.0",
"@fast-check/jest": "2.0.3",
"@hono/node-server": "1.19.0",
"@inquirer/prompts": "7.3.3",
"@jest/create-cache-key-function": "29.7.0",
"@jest/globals": "29.7.0",
"@jest/test-sequencer": "29.7.0",
"@libsql/client": "0.8.1",
"@neondatabase/serverless": "0.10.2",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "2.1.0",
"@opentelemetry/instrumentation": "0.206.0",
"@opentelemetry/resources": "2.1.0",
"@opentelemetry/sdk-trace-base": "2.1.0",
"@opentelemetry/semantic-conventions": "1.37.0",
"@planetscale/database": "1.19.0",
"@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/mini-proxy": "0.9.5",
"@prisma/query-compiler-wasm": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
"@prisma/query-engine-wasm": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773",
"@snaplet/copycat": "6.0.0",
"@swc-node/register": "1.10.9",
"@swc/core": "1.11.5",
"@swc/jest": "0.2.37",
"@timsuchanek/copy": "1.4.5",
"@types/debug": "4.1.12",
"@types/fs-extra": "11.0.4",
"@types/jest": "29.5.14",
"@types/js-levenshtein": "1.1.3",
"@types/mssql": "9.1.8",
"@types/node": "18.19.76",
"@types/pg": "8.11.11",
"arg": "5.0.2",
"benchmark": "2.1.4",
"cookie-es": "2.0.0",
"decimal.js": "10.5.0",
"execa": "8.0.1",
"expect-type": "1.2.2",
"fs-extra": "11.3.0",
"get-stream": "6.0.1",
"globby": "11.1.0",
"indent-string": "4.0.0",
"jest": "29.7.0",
"jest-extended": "4.0.2",
"jest-junit": "16.0.0",
"jest-serializer-ansi-escapes": "4.0.0",
"jest-snapshot": "29.7.0",
"js-levenshtein": "1.1.6",
"kleur": "4.1.5",
"klona": "2.0.6",
"mariadb": "3.4.5",
"memfs": "4.17.2",
"mssql": "11.0.1",
"new-github-issue-url": "0.2.1",
"p-retry": "4.6.2",
"pg": "8.14.1",
"resolve": "1.22.10",
"simple-statistics": "7.8.8",
"sort-keys": "5.1.0",
"source-map-support": "0.5.21",
"sql-template-tag": "5.2.1",
"stacktrace-parser": "0.1.11",
"strip-ansi": "7.1.0",
"strip-indent": "4.0.0",
"tempy": "3.0.0",
"ts-pattern": "5.6.2",
"tsd": "0.31.2",
"typescript": "5.4.5",
"undici": "7.4.0",
"zx": "8.4.1",
"@prisma/adapter-better-sqlite3": "6.19.3",
"@prisma/adapter-d1": "6.19.3",
"@prisma/adapter-libsql": "6.19.3",
"@prisma/adapter-mariadb": "6.19.3",
"@prisma/adapter-mssql": "6.19.3",
"@prisma/adapter-pg": "6.19.3",
"@prisma/adapter-planetscale": "6.19.3",
"@prisma/adapter-neon": "6.19.3",
"@prisma/client-common": "6.19.3",
"@prisma/client-engine-runtime": "6.19.3",
"@prisma/client-generator-ts": "6.19.3",
"@prisma/client-generator-js": "6.19.3",
"@prisma/config": "6.19.3",
"@prisma/dmmf": "6.19.3",
"@prisma/debug": "6.19.3",
"@prisma/engines": "6.19.3",
"@prisma/fetch-engine": "6.19.3",
"@prisma/driver-adapter-utils": "6.19.3",
"@prisma/generator": "6.19.3",
"@prisma/instrumentation": "6.19.3",
"@prisma/get-platform": "6.19.3",
"@prisma/internals": "6.19.3",
"@prisma/query-plan-executor": "6.19.3",
"@prisma/ts-builders": "6.19.3",
"@prisma/generator-helper": "6.19.3",
"@prisma/migrate": "6.19.3"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
},
"sideEffects": false,
"scripts": {
"dev": "DEV=true tsx helpers/build.ts",
"build": "tsx helpers/build.ts",
"test": "dotenv -e ../../.db.env -- jest --silent",
"test:e2e": "dotenv -e ../../.db.env -- tsx tests/e2e/_utils/run.ts",
"test:functional": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts",
"test:functional:client": "pnpm run test:functional --client-runtime client --engine-type client",
"test:memory": "dotenv -e ../../.db.env -- tsx helpers/memory-tests.ts",
"test:functional:code": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --no-types",
"test:functional:types": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --types-only",
"test-notypes": "dotenv -e ../../.db.env -- jest --testPathIgnorePatterns src/__tests__/types/types.test.ts",
"generate": "node scripts/postinstall.js",
"postinstall": "node scripts/postinstall.js",
"new-test": "tsx ./helpers/new-test/new-test.ts"
}
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/react-native'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/react-native'),
}
+1
View File
@@ -0,0 +1 @@
export * from "./library"
+286
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+293
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3982
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More