Datenbank-Repositories hinzugefügt
@@ -0,0 +1,248 @@
|
||||
# Datenbank-Repositories
|
||||
|
||||
← Zurück zur [Übersicht](Datenbank) · Tabellen: [Datenbank-Schema](Datenbank-Schema)
|
||||
|
||||
`src/db/repositories.js` enthält **alle** Datenbankzugriffe der App. Der Rest der App
|
||||
schreibt kein SQL, sondern ruft Funktionen aus dieser Datei auf.
|
||||
|
||||
```js
|
||||
import { createRepositories } from "./db/repositories.js";
|
||||
|
||||
const repositories = createRepositories(db, config);
|
||||
const chats = repositories.listChats("jerome");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Aufbau der Datei
|
||||
|
||||
| Teil | Inhalt |
|
||||
|---|---|
|
||||
| 1. Hilfsfunktionen | `parseJsonArray`, `buildPersonaSnapshot`, `MARKET_SORT_ORDERS`, `normalizeMarketSort`, `normalizeMarketPersonaType` |
|
||||
| 2. `statements` | Alle vorbereiteten SQL-Abfragen (`db.prepare`), einmal beim Start erstellt |
|
||||
| 3. Transaktionen | Zusammengehörige Befehle, die gemeinsam gelingen oder gemeinsam fehlschlagen |
|
||||
| 4. Rückgabeobjekt | Die öffentliche API, meist dünne Hüllen um ein Statement |
|
||||
|
||||
`createRepositories` ist eine **Factory-Funktion**: `statements` ist von außen nicht
|
||||
erreichbar, nur das zurückgegebene Objekt. Tests können eine eigene In-Memory-Datenbank übergeben.
|
||||
|
||||
Verwendete Config-Felder: `chatHistoryLimit` (Länge des Modellkontexts), `uploadMaxFiles` (Import).
|
||||
|
||||
---
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
**Zugriffsschutz über `username`.** Die meisten Abfragen enthalten `AND username = ?`.
|
||||
Ein Benutzer bekommt dadurch nur seine eigenen Daten, auch wenn er eine fremde ID schickt.
|
||||
Nachrichtenfunktionen (`listChatMessages(chatId)` usw.) prüfen den Benutzer **nicht**.
|
||||
Vorher immer mit `getChat(chatId, username)` sicherstellen, dass der Chat dem Benutzer gehört.
|
||||
|
||||
**Rückgabewerte.** Lesefunktionen geben ein Objekt, `undefined`/`null` oder ein Array zurück.
|
||||
Schreibfunktionen geben meist das Ergebnis von `.run()` zurück: `{ changes, lastInsertRowid }`.
|
||||
`changes === 0` bedeutet: nichts gefunden oder nicht berechtigt.
|
||||
|
||||
**`this` in einigen Funktionen.** `upsertMarketPersona`, `rateMarketPersona` und `exportWorkspace`
|
||||
benutzen `this`. Sie funktionieren nur als `repositories.funktion(...)`, **nicht** nach
|
||||
Destructuring (`const { exportWorkspace } = repositories` bricht).
|
||||
|
||||
**Parameterreihenfolge ist nicht einheitlich.** Meist steht `chatId` vorne, aber z. B.
|
||||
`updatePassword(passwordHash, username)` oder `createChatMemory(username, chat, fact)`.
|
||||
Im Zweifel in der Tabelle unten nachschlagen.
|
||||
|
||||
**Payloads in camelCase, Zeilen in snake_case.** Eingaben wie `createPersona(..., payload)`
|
||||
erwarten `exampleDialogues`, Rückgaben aus der DB enthalten `example_dialogues`.
|
||||
|
||||
### Transaktionen
|
||||
|
||||
| Transaktion | Wird verwendet von |
|
||||
|---|---|
|
||||
| `renameUserTransaction` | `renameUser` |
|
||||
| `deleteUserTransaction` | `deleteUser` |
|
||||
| `completeChatTurnTransaction` | `completeChatTurn` |
|
||||
| `insertBranchChatMessageTransaction` | `insertBranchChatMessage` |
|
||||
| `failChatTurnTransaction` | `failChatTurn` |
|
||||
| `savePersonaVersion` | `updatePersona`, `restorePersonaVersion` |
|
||||
| `clonePersonaTransaction` | `clonePersona` |
|
||||
| `importWorkspaceTransaction` | `importWorkspace` |
|
||||
| (inline) | `saveMessageVersion` |
|
||||
|
||||
---
|
||||
|
||||
## Funktionsreferenz
|
||||
|
||||
### Benutzer
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `getUser(username)` | Benutzer mit Passwort-Hash und `session_version`, sonst `undefined` |
|
||||
| `insertUser(username, passwordHash)` | Legt einen Benutzer an |
|
||||
| `updatePassword(passwordHash, username)` | Setzt einen neuen Hash. **Hash zuerst!** |
|
||||
| `incrementSessionVersion(username)` | Macht alle Sessions ungültig, gibt den aktualisierten Benutzer zurück |
|
||||
| `renameUser(currentUsername, nextUsername)` | Ändert den Namen in allen Tabellen (Transaktion) |
|
||||
| `deleteUser(username)` | Löscht den Benutzer und alle seine Daten (Transaktion) |
|
||||
|
||||
### Chats
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listChats(username)` | Alle Chats, angepinnte zuerst, dann nach `updated_at` |
|
||||
| `searchChats(username, query)` | Suche in Titel und Nachrichten (Groß-/Kleinschreibung egal). Enthält `matched_content`. Ein Chat kann **mehrfach** vorkommen (einmal pro Treffer) |
|
||||
| `countChats(username)` | Anzahl Chats |
|
||||
| `getChat(chatId, username)` | Ein Chat inkl. Persona-Namen oder `undefined` |
|
||||
| `getChatByParticipants(username, assistantPersonaId, userPersonaId)` | Hauptchat (keine Abzweigung) für diese Persona-Kombination |
|
||||
| `createChat(username, title, assistantPersonaId?, userPersonaId?, scenarioPrompt?, scenarioSummary?, organization?)` | Legt einen Chat an und gibt ihn zurück. `organization`: `{ folderName, isPinned, archivedAt }` |
|
||||
| `updateChatTitle(chatId, username, title)` | |
|
||||
| `updateChatScene(chatId, username, scenarioPrompt, scenarioSummary)` | |
|
||||
| `updateChatContextSummary(chatId, username, summary, messageId)` | Speichert die Verlaufszusammenfassung |
|
||||
| `updateChatOrganization(chatId, username, { folderName, isPinned, archivedAt })` | Ordner, Anpinnen, Archivieren |
|
||||
| `updateChatGenerationSettings(chatId, username, settings)` | `settings`: `preferredModel`, `temperature`, `contextLength`, `responseLength`, `systemInstruction` |
|
||||
| `updateChatPreferredModel(chatId, username, model)` | |
|
||||
| `updateChatActiveLeaf(chatId, username, messageId)` | Wechselt den sichtbaren Zweig |
|
||||
| `updateChatBranchSource(chatId, username, sourceChatId, messageId)` | Markiert einen Chat als Abzweigung |
|
||||
| `touchChat(chatId, username)` | Setzt nur `updated_at` (Chat rutscht in der Liste nach oben) |
|
||||
| `deleteChat(chatId, username)` | Löscht den Chat inkl. Nachrichten, Memories und Chat-State |
|
||||
|
||||
### Memories und Chat-State
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listChatMemories(chatId, username)` | Neueste zuerst |
|
||||
| `getChatMemory(chatId, memoryId, username)` | |
|
||||
| `createChatMemory(username, chat, fact)` | `chat` = Chat-Objekt (für die Persona-IDs). Gibt den neuen Eintrag zurück |
|
||||
| `updateChatMemory(chatId, memoryId, username, fact)` | |
|
||||
| `deleteChatMemory(chatId, memoryId, username)` | |
|
||||
| `getPersonaChatState(chatId, username)` | Rollenspiel-Zustand oder `null` |
|
||||
| `updatePersonaChatState(username, chat, state)` | Upsert. `state` in snake_case (`relationship_notes`, …) |
|
||||
|
||||
### Nachrichten lesen
|
||||
|
||||
Hintergrund zu Zweigen: [Datenbank-Schema → Nachrichtenbaum](Datenbank-Schema#nachrichtenbaum)
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listChatMessages(chatId)` | Alle Nachrichten **aller** Zweige, nach ID |
|
||||
| `listActiveBranchMessages(chatId)` | Nur der sichtbare Zweig, von der Wurzel bis zum aktiven Blatt |
|
||||
| `listChatBranchThroughMessage(chatId, messageId)` | Zweig von der Wurzel bis `messageId`, nur `complete` |
|
||||
| `getRecentChatMessages(chatId)` | Letzte `chatHistoryLimit` Nachrichten des aktiven Zweigs ohne `failed`, **neueste zuerst**, nur `id`, `role`, `content`. Grundlage für den Modellkontext |
|
||||
| `getRecentChatMessagesUpToId(chatId, messageId)` | Wie oben, endet aber bei `messageId` (z. B. beim Regenerieren) |
|
||||
| `listSiblingChatMessages(chatId, role, parentMessageId)` | Alternativen mit gleichem Parent und gleicher Rolle |
|
||||
| `getLatestDescendantMessage(chatId, messageId)` | Neuester Nachfahre ohne `failed`, also das Blatt beim Zweigwechsel |
|
||||
| `getChatMessage(chatId, messageId)` | Eine Nachricht |
|
||||
| `getChatMessageByIndex(chatId, index)` | n-te Nachricht nach ID, über alle Zweige |
|
||||
| `getLatestChatMessage(chatId)` | Nachricht mit der höchsten ID, zweigunabhängig |
|
||||
| `getPreviousUserMessage(chatId, messageId)` | Letzte User-Nachricht mit kleinerer ID, zweigunabhängig |
|
||||
| `listMessageVersions(messageId)` | Alle Einträge aus `chat_message_versions` |
|
||||
|
||||
### Nachrichten schreiben
|
||||
|
||||
`retryState` ist ein Objekt `{ retryVariants, retryActiveIndex, retryRetriesUsed, retryPromptMessageId }`.
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `insertChatMessage(chatId, role, content, retryState?, modelName?, deliveryStatus?, errorMessage?, parentMessageId?)` | Fügt eine Nachricht ein, ändert **nicht** das aktive Blatt |
|
||||
| `insertBranchChatMessage(...)` | Gleiche Parameter. Fügt ein **und** setzt das aktive Blatt (Transaktion) |
|
||||
| `completeChatTurn(chatId, userMessageId, assistantContent, retryState, modelName?, sources?, thinking?)` | Speichert die Bot-Antwort als Kind der User-Nachricht, markiert die User-Nachricht als `complete`, speichert Quellen und Thinking, setzt das aktive Blatt (Transaktion) |
|
||||
| `failChatTurn(chatId, userMessageId, errorMessage, parentMessageId?)` | Markiert die User-Nachricht als `failed` und setzt das aktive Blatt zurück auf den Parent (Transaktion) |
|
||||
| `updateChatMessageDelivery(chatId, messageId, deliveryStatus, errorMessage?)` | |
|
||||
| `updateChatMessage(chatId, messageId, content)` | Nur den Text ändern |
|
||||
| `updateChatMessageRetryState(chatId, messageId, payload)` | `payload`: `content`, `retryVariants`, `retryActiveIndex`, `retryRetriesUsed`, `retryPromptMessageId` |
|
||||
| `updateChatMessageThinking(chatId, messageId, thinking)` | |
|
||||
| `updateChatMessageSources(chatId, messageId, sources)` | `sources` wird als JSON gespeichert |
|
||||
| `saveMessageVersion(chatId, messageId, version?, selectedIndex?)` | Mit `version`: neue Fassung anhängen und anzeigen. Ohne: Fassung `selectedIndex` anzeigen. Überträgt die Fassung nach `chat_messages` und baut `retry_variants` neu auf. Wirft `"Message not found"` bzw. `"Version is out of range"` |
|
||||
| `deleteChatMessage(chatId, messageId)` | Kind-Nachrichten bleiben erhalten, ihr Parent wird `NULL` |
|
||||
| `clearChatMessages(chatId)` | Löscht alle Nachrichten des Chats |
|
||||
|
||||
### Personas
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listPersonasByType(username, personaType)` | `personaType`: `assistant` oder `user` |
|
||||
| `listAllPersonas(username)` | |
|
||||
| `getPersona(personaId, username)` | |
|
||||
| `createPersona(username, payload, personaType)` | Legt an, speichert Version 1, gibt die Persona zurück |
|
||||
| `updatePersona(personaId, username, payload)` | Speichert den **alten** Stand als Version, dann Update |
|
||||
| `listPersonaVersions(personaId, username)` | Neueste zuerst, mit geparstem `snapshot` |
|
||||
| `restorePersonaVersion(personaId, versionId, username)` | Sichert den aktuellen Stand, stellt die Version wieder her. `null`, wenn nicht gefunden |
|
||||
| `clonePersona(personaId, username)` | Kopie mit Namenszusatz „ Copy“ (Transaktion) |
|
||||
| `deletePersona(personaId, username)` | Vorher `clearPersonaReferences` aufrufen |
|
||||
| `clearPersonaReferences(username, personaId)` | Entfernt die Persona aus Chats und löscht ihren Marktplatz-Eintrag |
|
||||
| `getActivePersonaIds(username)` | `{ active_persona_id, active_user_persona_id }` oder `{}` |
|
||||
| `setActiveUserPersona(username, personaId)` | Upsert in `user_settings` |
|
||||
| `getActiveUserPersona(username)` | |
|
||||
| `getAssistantPersonaForChat(chatId, username)` | |
|
||||
| `getUserPersonaForChat(chatId, username)` | |
|
||||
|
||||
### Marktplatz
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listMarketPersonas({ admin, username, personaType, sort })` | Liste mit `is_favorite` (für `username`) und geparsten `tags`. Ohne `admin` werden gelöschte und abgelehnte Einträge ausgeblendet. `sort`: `best`, `newest`, `most_favorited`, `most_popular`, `top_rated`, `alphabetical` (Bindestriche erlaubt, unbekannt → `best`) |
|
||||
| `getMarketPersona(marketId)` | Mit `rating_average` und geparsten `tags`, sonst `null` |
|
||||
| `getMarketPersonaByPersonaId(personaId, username)` | Marktplatz-Eintrag zu einer eigenen Persona |
|
||||
| `upsertMarketPersona(personaId, username, persona, tags?)` | Veröffentlichen oder aktualisieren. Setzt Moderation auf `approved` zurück und hebt Soft Delete auf |
|
||||
| `deletePublishedPersona(personaId, username)` | Entfernt den Eintrag endgültig |
|
||||
| `listPublishedPersonaIds(username)` | Array der veröffentlichten Persona-IDs |
|
||||
| `collectMarketPersona(username, marketPersona)` | Kopiert einen Eintrag in die eigenen Personas (mit `source_market_id`), speichert Version 1 |
|
||||
| `getPersonaBySourceMarket(username, marketId)` | Prüft, ob bereits gesammelt |
|
||||
| `incrementMarketUsageCount(marketId)` | |
|
||||
| `toggleMarketFavorite(marketId, username)` | Gibt den **neuen** Zustand zurück (`true` = jetzt Favorit) |
|
||||
| `rateMarketPersona(marketId, username, rating)` | Upsert, zählt neu, gibt den Eintrag zurück |
|
||||
| `reportMarketPersona(marketId, username, reason, details)` | |
|
||||
| `listMarketReports()` | Offene Meldungen zuerst |
|
||||
| `updateMarketReportStatus(reportId, status)` | |
|
||||
| `updateMarketModeration(marketId, { status, notes?, softDelete? })` | Für Admins |
|
||||
|
||||
### Prompt-Vorlagen
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `listPromptTemplates(username)` | Eigene, geteilte und System-Vorlagen |
|
||||
| `listPromptTemplatesForPersona(username, personaId?)` | Vorlagen für diese Persona plus allgemeine |
|
||||
| `getPromptTemplate(templateId)` | **Ohne Benutzerprüfung** |
|
||||
| `createPromptTemplate(username, payload)` | `payload` in camelCase (`promptText`, `starterText`, `isShared`, `personaId`) |
|
||||
| `updatePromptTemplate(templateId, username, payload)` | Nur eigene. System-Vorlagen nur als Benutzer `system` |
|
||||
| `deletePromptTemplate(templateId, username)` | Nur eigene |
|
||||
|
||||
### Dashboard, Export und Import
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|---|---|
|
||||
| `getDashboardSummary(username)` | Statistiken: Anzahl Chats, Nachrichten, Personas, veröffentlichte Personas, meistgenutzte Personas, durchschnittliche Chatlänge, Lieblingsmodell, letzte Aktivität |
|
||||
| `exportWorkspace(username)` | Alle Chats (mit Nachrichten, Versionen, Anhang-Metadaten, Memories, Chat-State), Personas und Vorlagen als Objekt. Bilddateien sind **nicht** enthalten |
|
||||
| `importWorkspace(username, workspace)` | Importiert in einer Transaktion. Gibt `{ personas, chats, templates }` (Anzahl) zurück |
|
||||
|
||||
**Import-Details:**
|
||||
|
||||
- Nachrichten-IDs werden neu vergeben. Die Zuordnung alt → neu läuft über `message.sourceId`,
|
||||
damit Parents, Retry-Verweise und das aktive Blatt stimmen.
|
||||
- Anhänge werden nur verknüpft, wenn das Bild noch existiert und dem Benutzer gehört.
|
||||
- Importierte Chats und Memories verlieren ihre Persona-Verknüpfung (`NULL`).
|
||||
- Der Export liefert Felder in snake_case (`id`, `parent_message_id`), der Import erwartet
|
||||
teils camelCase (`sourceId`, `parentMessageId`). Die Umwandlung passiert vermutlich in
|
||||
`workspaceImportService.js`. Bei Änderungen beide Seiten prüfen.
|
||||
|
||||
---
|
||||
|
||||
## Hilfsfunktionen
|
||||
|
||||
| Name | Aufgabe |
|
||||
|---|---|
|
||||
| `parseJsonArray(value)` | Parst JSON sicher. Gibt bei `null`, ungültigem JSON oder Nicht-Arrays `[]` zurück |
|
||||
| `buildPersonaSnapshot(persona)` | Erzeugt das JSON für `persona_versions.snapshot_json` |
|
||||
| `MARKET_SORT_ORDERS` | Erlaubte Sortierungen für den Marktplatz (Whitelist, wird direkt ins SQL eingesetzt) |
|
||||
| `normalizeMarketSort(sort)` | Bereinigt die Sortierung, unbekannt → `best` |
|
||||
| `normalizeMarketPersonaType(type)` | Nur `assistant` oder `user`, sonst `null` |
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Auffälligkeiten
|
||||
|
||||
| Punkt | Details |
|
||||
|---|---|
|
||||
| Doppelter SELECT | `listChatSessions`, `getChatSession`, `getChatSessionByParticipants` und `searchChats` wiederholen denselben Spaltenblock |
|
||||
| `db.prepare` außerhalb von `statements` | In `listMessageVersions`, `saveMessageVersion`, `exportWorkspace` und in den Import-Schleifen wird bei jedem Aufruf neu vorbereitet |
|
||||
| Ungenutztes Statement | `listMarketPersonasAdmin` wird nirgends verwendet |
|
||||
| Dashboard zählt teuer | `marketPersonas` lädt alle Marktplatz-Einträge, nur um sie zu zählen |
|
||||
| Moderation beim erneuten Veröffentlichen | `upsertMarketPersona` setzt den Status auf `approved`. Eine abgelehnte Persona wird durch erneutes Veröffentlichen wieder sichtbar. Prüfen, ob das gewollt ist |
|
||||
|
||||
Offene Aufgaben für Aufräumen und TypeScript-Migration stehen als TODO-Kommentar oben in `repositories.js`.
|
||||
Reference in New Issue
Block a user