Clone
1
Datenbank-Repositories
ScriptSorcerer edited this page 2026-09-23 23:35:36 +02:00

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 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.