Datenbank-Repositories
← Zurück zur Übersicht · Tabellen: 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.
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
| 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 inworkspaceImportService.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.