This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/sqlite/export.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

1516 lines
52 KiB
Go

package sqlite
// export.go — v0.34.0 CS0
//
// ExportStore: batch-read queries for data export and GDPR operations.
// SQLite dialect — uses ? placeholders, st() for time scanning, ScanArray for JSON arrays.
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"switchboard-core/models"
)
// ExportStore implements store.ExportStore for SQLite.
type ExportStore struct{}
// NewExportStore creates a SQLite ExportStore.
func NewExportStore() *ExportStore { return &ExportStore{} }
// ── helpers ──────────────────────────────────
// sqlitePlaceholders builds "?,?,...,?" and a matching []any slice for IN queries.
func sqlitePlaceholders(ids []string) (string, []any) {
ph := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
ph[i] = "?"
args[i] = id
}
return strings.Join(ph, ","), args
}
// ── User-scoped export ───────────────────────
func (s *ExportStore) UserChannels(ctx context.Context, userID string) ([]models.Channel, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, title, description, type, model, system_prompt,
provider_config_id, is_archived, is_pinned, folder_id,
team_id, project_id, workspace_id, settings, allow_anonymous,
created_at, updated_at
FROM channels WHERE user_id = ?
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user channels: %w", err)
}
defer rows.Close()
var out []models.Channel
for rows.Next() {
var ch models.Channel
if err := rows.Scan(&ch.ID, &ch.UserID, &ch.Title, &ch.Description,
&ch.Type, &ch.Model, &ch.SystemPrompt, &ch.ProviderConfigID,
&ch.IsArchived, &ch.IsPinned, &ch.FolderID, &ch.TeamID,
&ch.ProjectID, &ch.WorkspaceID, &ch.Settings, &ch.AllowAnonymous,
st(&ch.CreatedAt), st(&ch.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan channel: %w", err)
}
out = append(out, ch)
}
return out, rows.Err()
}
func (s *ExportStore) UserMessages(ctx context.Context, channelIDs []string) ([]models.Message, error) {
if len(channelIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(channelIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, channel_id, role, content, model, tokens_used,
tool_calls, metadata, parent_id, sibling_index,
participant_type, participant_id, provider_config_id,
created_at, updated_at
FROM messages
WHERE channel_id IN (%s) AND deleted_at IS NULL
ORDER BY created_at ASC`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: user messages: %w", err)
}
defer rows.Close()
var out []models.Message
for rows.Next() {
var m models.Message
if err := rows.Scan(&m.ID, &m.ChannelID, &m.Role, &m.Content,
&m.Model, &m.TokensUsed, &m.ToolCalls, &m.Metadata,
&m.ParentID, &m.SiblingIndex, &m.ParticipantType,
&m.ParticipantID, &m.ProviderConfigID,
st(&m.CreatedAt), st(&m.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan message: %w", err)
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *ExportStore) UserChannelParticipants(ctx context.Context, channelIDs []string) ([]models.ChannelParticipant, error) {
if len(channelIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(channelIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, channel_id, participant_type, participant_id, role,
display_name, avatar_url, joined_at, last_read_at
FROM channel_participants WHERE channel_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: channel participants: %w", err)
}
defer rows.Close()
var out []models.ChannelParticipant
for rows.Next() {
var cp models.ChannelParticipant
if err := rows.Scan(&cp.ID, &cp.ChannelID, &cp.ParticipantType,
&cp.ParticipantID, &cp.Role, &cp.DisplayName, &cp.AvatarURL,
st(&cp.JoinedAt), st(&cp.LastReadAt)); err != nil {
return nil, fmt.Errorf("export: scan participant: %w", err)
}
out = append(out, cp)
}
return out, rows.Err()
}
func (s *ExportStore) UserChannelModels(ctx context.Context, channelIDs []string) ([]models.ChannelModel, error) {
if len(channelIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(channelIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, channel_id, model_id, provider_config_id, persona_id,
handle, display_name, system_prompt, is_default
FROM channel_models WHERE channel_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: channel models: %w", err)
}
defer rows.Close()
var out []models.ChannelModel
for rows.Next() {
var cm models.ChannelModel
if err := rows.Scan(&cm.ID, &cm.ChannelID, &cm.ModelID,
&cm.ProviderConfigID, &cm.PersonaID, &cm.Handle,
&cm.DisplayName, &cm.SystemPrompt, &cm.IsDefault); err != nil {
return nil, fmt.Errorf("export: scan channel model: %w", err)
}
out = append(out, cm)
}
return out, rows.Err()
}
func (s *ExportStore) UserChannelCursors(ctx context.Context, userID string, channelIDs []string) ([]models.ChannelCursor, error) {
if len(channelIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(channelIDs)
args = append(args, userID)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, channel_id, user_id, active_leaf_id, updated_at
FROM channel_cursors
WHERE channel_id IN (%s) AND user_id = ?`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: channel cursors: %w", err)
}
defer rows.Close()
var out []models.ChannelCursor
for rows.Next() {
var cc models.ChannelCursor
if err := rows.Scan(&cc.ID, &cc.ChannelID, &cc.UserID,
&cc.ActiveLeafID, st(&cc.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan cursor: %w", err)
}
out = append(out, cc)
}
return out, rows.Err()
}
func (s *ExportStore) UserNotes(ctx context.Context, userID string) ([]models.Note, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, title, content, folder_path, tags,
metadata, source_channel_id, source_message_id, team_id,
created_at, updated_at
FROM notes WHERE user_id = ?
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user notes: %w", err)
}
defer rows.Close()
var out []models.Note
for rows.Next() {
var n models.Note
var tagsJSON, metadataJSON string
var sourceChannelID, sourceMessageID, teamID sql.NullString
if err := rows.Scan(&n.ID, &n.UserID, &n.Title, &n.Content,
&n.FolderPath, &tagsJSON, &metadataJSON,
&sourceChannelID, &sourceMessageID, &teamID,
st(&n.CreatedAt), st(&n.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan note: %w", err)
}
n.Tags = ScanArray(tagsJSON)
n.SourceChannelID = NullableStringPtr(sourceChannelID)
n.SourceMessageID = NullableStringPtr(sourceMessageID)
n.TeamID = NullableStringPtr(teamID)
out = append(out, n)
}
return out, rows.Err()
}
func (s *ExportStore) UserNoteLinks(ctx context.Context, noteIDs []string) ([]models.ExportNoteLink, error) {
if len(noteIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(noteIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT source_note_id, target_note_id, target_title,
COALESCE(display_text, ''), is_transclusion
FROM note_links WHERE source_note_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: note links: %w", err)
}
defer rows.Close()
var out []models.ExportNoteLink
for rows.Next() {
var nl models.ExportNoteLink
if err := rows.Scan(&nl.SourceNoteID, &nl.TargetNoteID,
&nl.TargetTitle, &nl.DisplayText, &nl.IsTransclusion); err != nil {
return nil, fmt.Errorf("export: scan note link: %w", err)
}
out = append(out, nl)
}
return out, rows.Err()
}
func (s *ExportStore) UserMemories(ctx context.Context, userID string) ([]models.Memory, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at
FROM memories
WHERE owner_id = ? AND scope = 'user' AND status = 'active'
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user memories: %w", err)
}
defer rows.Close()
var out []models.Memory
for rows.Next() {
var m models.Memory
if err := rows.Scan(&m.ID, &m.Scope, &m.OwnerID, &m.UserID,
&m.Key, &m.Value, &m.SourceChannelID, &m.Confidence,
&m.Status, st(&m.CreatedAt), st(&m.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan memory: %w", err)
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *ExportStore) UserProjects(ctx context.Context, userID string) ([]models.Project, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, description, color, icon, scope, owner_id,
team_id, is_archived, workspace_id, settings, created_at, updated_at
FROM projects WHERE owner_id = ? AND scope = 'personal'
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user projects: %w", err)
}
defer rows.Close()
var out []models.Project
for rows.Next() {
var p models.Project
if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Color,
&p.Icon, &p.Scope, &p.OwnerID, &p.TeamID, &p.IsArchived,
&p.WorkspaceID, &p.Settings, st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan project: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *ExportStore) UserProjectChannels(ctx context.Context, projectIDs []string) ([]models.ProjectChannel, error) {
if len(projectIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(projectIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT project_id, channel_id, position, folder, added_at
FROM project_channels WHERE project_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: project channels: %w", err)
}
defer rows.Close()
var out []models.ProjectChannel
for rows.Next() {
var pc models.ProjectChannel
if err := rows.Scan(&pc.ProjectID, &pc.ChannelID, &pc.Position,
&pc.Folder, &pc.AddedAt); err != nil {
return nil, fmt.Errorf("export: scan project channel: %w", err)
}
out = append(out, pc)
}
return out, rows.Err()
}
func (s *ExportStore) UserProjectKBs(ctx context.Context, projectIDs []string) ([]models.ProjectKB, error) {
if len(projectIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(projectIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT project_id, kb_id, auto_search, added_at
FROM project_knowledge_bases WHERE project_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: project KBs: %w", err)
}
defer rows.Close()
var out []models.ProjectKB
for rows.Next() {
var pk models.ProjectKB
if err := rows.Scan(&pk.ProjectID, &pk.KBID, &pk.AutoSearch, &pk.AddedAt); err != nil {
return nil, fmt.Errorf("export: scan project KB: %w", err)
}
out = append(out, pk)
}
return out, rows.Err()
}
func (s *ExportStore) UserProjectNotes(ctx context.Context, projectIDs []string) ([]models.ProjectNote, error) {
if len(projectIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(projectIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT project_id, note_id, added_at
FROM project_notes WHERE project_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: project notes: %w", err)
}
defer rows.Close()
var out []models.ProjectNote
for rows.Next() {
var pn models.ProjectNote
if err := rows.Scan(&pn.ProjectID, &pn.NoteID, &pn.AddedAt); err != nil {
return nil, fmt.Errorf("export: scan project note: %w", err)
}
out = append(out, pn)
}
return out, rows.Err()
}
func (s *ExportStore) UserWorkspaces(ctx context.Context, userID string) ([]models.Workspace, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, owner_type, owner_id, name, root_path, max_bytes, status,
indexing_enabled, git_remote_url, git_branch, git_credential_id,
git_last_sync, created_at, updated_at
FROM workspaces WHERE owner_type = 'user' AND owner_id = ?
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user workspaces: %w", err)
}
defer rows.Close()
var out []models.Workspace
for rows.Next() {
var w models.Workspace
if err := rows.Scan(&w.ID, &w.OwnerType, &w.OwnerID, &w.Name,
&w.RootPath, &w.MaxBytes, &w.Status, &w.IndexingEnabled,
&w.GitRemoteURL, &w.GitBranch, &w.GitCredentialID,
stN(&w.GitLastSync), st(&w.CreatedAt), st(&w.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan workspace: %w", err)
}
out = append(out, w)
}
return out, rows.Err()
}
func (s *ExportStore) UserWorkspaceFiles(ctx context.Context, workspaceIDs []string) ([]models.WorkspaceFile, error) {
if len(workspaceIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(workspaceIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, workspace_id, path, is_directory, content_type,
size_bytes, sha256, index_status, chunk_count,
created_at, updated_at
FROM workspace_files WHERE workspace_id IN (%s)
ORDER BY path ASC`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: workspace files: %w", err)
}
defer rows.Close()
var out []models.WorkspaceFile
for rows.Next() {
var wf models.WorkspaceFile
if err := rows.Scan(&wf.ID, &wf.WorkspaceID, &wf.Path,
&wf.IsDirectory, &wf.ContentType, &wf.SizeBytes,
&wf.SHA256, &wf.IndexStatus, &wf.ChunkCount,
st(&wf.CreatedAt), st(&wf.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan workspace file: %w", err)
}
out = append(out, wf)
}
return out, rows.Err()
}
func (s *ExportStore) UserFiles(ctx context.Context, userID string) ([]models.File, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, channel_id, message_id, user_id, project_id,
origin, filename, content_type, size_bytes, storage_key,
display_hint, extracted_text, metadata, created_at, updated_at
FROM files WHERE user_id = ?
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user files: %w", err)
}
defer rows.Close()
var out []models.File
for rows.Next() {
var f models.File
if err := rows.Scan(&f.ID, &f.ChannelID, &f.MessageID, &f.UserID,
&f.ProjectID, &f.Origin, &f.Filename, &f.ContentType,
&f.SizeBytes, &f.StorageKey, &f.DisplayHint,
&f.ExtractedText, &f.Metadata, st(&f.CreatedAt), st(&f.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan file: %w", err)
}
out = append(out, f)
}
return out, rows.Err()
}
func (s *ExportStore) UserFolders(ctx context.Context, userID string) ([]models.Folder, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, name, parent_id, sort_order, created_at, updated_at
FROM folders WHERE user_id = ?
ORDER BY sort_order ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: user folders: %w", err)
}
defer rows.Close()
var out []models.Folder
for rows.Next() {
var f models.Folder
if err := rows.Scan(&f.ID, &f.UserID, &f.Name, &f.ParentID,
&f.SortOrder, st(&f.CreatedAt), st(&f.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan folder: %w", err)
}
out = append(out, f)
}
return out, rows.Err()
}
func (s *ExportStore) UserModelSettings(ctx context.Context, userID string) ([]models.UserModelSetting, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, model_id, provider_config_id, hidden,
preferred_temperature, preferred_max_tokens, sort_order,
created_at, updated_at
FROM user_model_settings WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: user model settings: %w", err)
}
defer rows.Close()
var out []models.UserModelSetting
for rows.Next() {
var ums models.UserModelSetting
if err := rows.Scan(&ums.ID, &ums.UserID, &ums.ModelID, &ums.ProviderConfigID,
&ums.Hidden, &ums.PreferredTemperature, &ums.PreferredMaxTokens,
&ums.SortOrder, st(&ums.CreatedAt), st(&ums.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan user model setting: %w", err)
}
out = append(out, ums)
}
return out, rows.Err()
}
func (s *ExportStore) UserNotifPrefs(ctx context.Context, userID string) ([]models.NotificationPreference, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, type, in_app, email
FROM notification_preferences WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: notification prefs: %w", err)
}
defer rows.Close()
var out []models.NotificationPreference
for rows.Next() {
var np models.NotificationPreference
if err := rows.Scan(&np.ID, &np.UserID, &np.Type, &np.InApp, &np.Email); err != nil {
return nil, fmt.Errorf("export: scan notif pref: %w", err)
}
out = append(out, np)
}
return out, rows.Err()
}
func (s *ExportStore) UserUsageEntries(ctx context.Context, userID string) ([]models.UsageEntry, error) {
cutoff := time.Now().AddDate(0, 0, -90).Format(timeFmt)
rows, err := DB.QueryContext(ctx, `
SELECT id, channel_id, user_id, provider_config_id, provider_scope,
model_id, role, prompt_tokens, completion_tokens,
cache_creation_tokens, cache_read_tokens,
cost_input, cost_output, routing_decision, created_at
FROM usage_log
WHERE user_id = ? AND created_at >= ?
ORDER BY created_at ASC`, userID, cutoff)
if err != nil {
return nil, fmt.Errorf("export: usage entries: %w", err)
}
defer rows.Close()
var out []models.UsageEntry
for rows.Next() {
var ue models.UsageEntry
if err := rows.Scan(&ue.ID, &ue.ChannelID, &ue.UserID,
&ue.ProviderConfigID, &ue.ProviderScope, &ue.ModelID,
&ue.Role, &ue.PromptTokens, &ue.CompletionTokens,
&ue.CacheCreationTokens, &ue.CacheReadTokens,
&ue.CostInput, &ue.CostOutput, &ue.RoutingDecision,
st(&ue.CreatedAt)); err != nil {
return nil, fmt.Errorf("export: scan usage entry: %w", err)
}
out = append(out, ue)
}
return out, rows.Err()
}
func (s *ExportStore) UserPersonaGroups(ctx context.Context, userID string) ([]models.PersonaGroup, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, description, owner_id, scope, team_id,
created_at, updated_at
FROM persona_groups WHERE owner_id = ?
ORDER BY created_at ASC`, userID)
if err != nil {
return nil, fmt.Errorf("export: persona groups: %w", err)
}
defer rows.Close()
var out []models.PersonaGroup
for rows.Next() {
var pg models.PersonaGroup
if err := rows.Scan(&pg.ID, &pg.Name, &pg.Description, &pg.OwnerID,
&pg.Scope, &pg.TeamID, st(&pg.CreatedAt), st(&pg.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan persona group: %w", err)
}
out = append(out, pg)
}
return out, rows.Err()
}
func (s *ExportStore) UserPersonaGroupMembers(ctx context.Context, groupIDs []string) ([]models.PersonaGroupMember, error) {
if len(groupIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(groupIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, group_id, persona_id, is_leader, sort_order
FROM persona_group_members WHERE group_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: persona group members: %w", err)
}
defer rows.Close()
var out []models.PersonaGroupMember
for rows.Next() {
var pgm models.PersonaGroupMember
if err := rows.Scan(&pgm.ID, &pgm.GroupID, &pgm.PersonaID,
&pgm.IsLeader, &pgm.SortOrder); err != nil {
return nil, fmt.Errorf("export: scan persona group member: %w", err)
}
out = append(out, pgm)
}
return out, rows.Err()
}
// ── Team-scoped export ───────────────────────
func (s *ExportStore) TeamChannels(ctx context.Context, teamID string) ([]models.Channel, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, user_id, title, description, type, model, system_prompt,
provider_config_id, is_archived, is_pinned, folder_id,
team_id, project_id, workspace_id, settings, allow_anonymous,
created_at, updated_at
FROM channels WHERE team_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team channels: %w", err)
}
defer rows.Close()
var out []models.Channel
for rows.Next() {
var ch models.Channel
if err := rows.Scan(&ch.ID, &ch.UserID, &ch.Title, &ch.Description,
&ch.Type, &ch.Model, &ch.SystemPrompt, &ch.ProviderConfigID,
&ch.IsArchived, &ch.IsPinned, &ch.FolderID, &ch.TeamID,
&ch.ProjectID, &ch.WorkspaceID, &ch.Settings, &ch.AllowAnonymous,
st(&ch.CreatedAt), st(&ch.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan team channel: %w", err)
}
out = append(out, ch)
}
return out, rows.Err()
}
func (s *ExportStore) TeamMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) {
rows, err := DB.QueryContext(ctx, `
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at,
u.email, u.display_name, u.username, u.role
FROM team_members tm
JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ?`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team members: %w", err)
}
defer rows.Close()
var out []models.TeamMember
for rows.Next() {
var tm models.TeamMember
if err := rows.Scan(&tm.ID, &tm.TeamID, &tm.UserID, &tm.Role,
&tm.JoinedAt, &tm.Email, &tm.DisplayName, &tm.Username,
&tm.UserRole); err != nil {
return nil, fmt.Errorf("export: scan team member: %w", err)
}
out = append(out, tm)
}
return out, rows.Err()
}
func (s *ExportStore) TeamPersonas(ctx context.Context, teamID string) ([]models.Persona, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, handle, description, icon, avatar,
base_model_id, provider_config_id,
system_prompt, temperature, max_tokens, thinking_budget, top_p,
scope, owner_id, created_by, is_active, is_shared,
memory_enabled, memory_extraction_prompt,
created_at, updated_at
FROM personas WHERE scope = 'team' AND owner_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team personas: %w", err)
}
defer rows.Close()
var out []models.Persona
for rows.Next() {
var p models.Persona
if err := rows.Scan(&p.ID, &p.Name, &p.Handle, &p.Description,
&p.Icon, &p.Avatar, &p.BaseModelID, &p.ProviderConfigID,
&p.SystemPrompt, &p.Temperature, &p.MaxTokens,
&p.ThinkingBudget, &p.TopP, &p.Scope, &p.OwnerID,
&p.CreatedBy, &p.IsActive, &p.IsShared,
&p.MemoryEnabled, &p.MemoryExtractionPrompt,
st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan persona: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
func (s *ExportStore) TeamPersonaKBs(ctx context.Context, personaIDs []string) ([]models.PersonaKB, error) {
if len(personaIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(personaIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT persona_id, kb_id, auto_search, added_at
FROM persona_knowledge_bases WHERE persona_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: persona KBs: %w", err)
}
defer rows.Close()
var out []models.PersonaKB
for rows.Next() {
var pk models.PersonaKB
if err := rows.Scan(&pk.PersonaID, &pk.KBID, &pk.AutoSearch, st(&pk.AddedAt)); err != nil {
return nil, fmt.Errorf("export: scan persona KB: %w", err)
}
out = append(out, pk)
}
return out, rows.Err()
}
func (s *ExportStore) TeamKnowledgeBases(ctx context.Context, teamID string) ([]models.KnowledgeBase, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, description, scope, owner_id, team_id,
embedding_config, document_count, chunk_count, total_bytes,
discoverable, status, created_at, updated_at
FROM knowledge_bases WHERE team_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team KBs: %w", err)
}
defer rows.Close()
var out []models.KnowledgeBase
for rows.Next() {
var kb models.KnowledgeBase
if err := rows.Scan(&kb.ID, &kb.Name, &kb.Description, &kb.Scope,
&kb.OwnerID, &kb.TeamID, &kb.EmbeddingConfig, &kb.DocumentCount,
&kb.ChunkCount, &kb.TotalBytes, &kb.Discoverable, &kb.Status,
st(&kb.CreatedAt), st(&kb.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan KB: %w", err)
}
out = append(out, kb)
}
return out, rows.Err()
}
func (s *ExportStore) TeamKBDocuments(ctx context.Context, kbIDs []string) ([]models.KBDocument, error) {
if len(kbIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(kbIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, kb_id, filename, content_type, size_bytes, storage_key,
chunk_count, status, error, uploaded_by, created_at, updated_at
FROM kb_documents WHERE kb_id IN (%s)
ORDER BY created_at ASC`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: KB documents: %w", err)
}
defer rows.Close()
var out []models.KBDocument
for rows.Next() {
var d models.KBDocument
if err := rows.Scan(&d.ID, &d.KBID, &d.Filename, &d.ContentType,
&d.SizeBytes, &d.StorageKey, &d.ChunkCount, &d.Status,
&d.Error, &d.UploadedBy, st(&d.CreatedAt), st(&d.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan KB document: %w", err)
}
out = append(out, d)
}
return out, rows.Err()
}
func (s *ExportStore) TeamWorkflows(ctx context.Context, teamID string) ([]models.Workflow, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, team_id, name, slug, description, branding,
entry_mode, is_active, version, on_complete, retention,
webhook_url, webhook_secret, created_by, created_at, updated_at
FROM workflows WHERE team_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team workflows: %w", err)
}
defer rows.Close()
var out []models.Workflow
for rows.Next() {
var w models.Workflow
if err := rows.Scan(&w.ID, &w.TeamID, &w.Name, &w.Slug,
&w.Description, &w.Branding, &w.EntryMode, &w.IsActive,
&w.Version, &w.OnComplete, &w.Retention, &w.WebhookURL,
&w.WebhookSecret, &w.CreatedBy, st(&w.CreatedAt), st(&w.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan workflow: %w", err)
}
out = append(out, w)
}
return out, rows.Err()
}
func (s *ExportStore) TeamWorkflowVersions(ctx context.Context, workflowIDs []string) ([]models.WorkflowVersion, error) {
if len(workflowIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(workflowIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, workflow_id, version_number, snapshot, created_at
FROM workflow_versions WHERE workflow_id IN (%s)
ORDER BY version_number ASC`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: workflow versions: %w", err)
}
defer rows.Close()
var out []models.WorkflowVersion
for rows.Next() {
var wv models.WorkflowVersion
if err := rows.Scan(&wv.ID, &wv.WorkflowID, &wv.VersionNumber,
&wv.Snapshot, st(&wv.CreatedAt)); err != nil {
return nil, fmt.Errorf("export: scan workflow version: %w", err)
}
out = append(out, wv)
}
return out, rows.Err()
}
func (s *ExportStore) TeamWorkflowStages(ctx context.Context, workflowIDs []string) ([]models.WorkflowStage, error) {
if len(workflowIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(workflowIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, workflow_id, ordinal, name, persona_id,
assignment_team_id, form_template, stage_mode,
history_mode, auto_transition, transition_rules,
surface_pkg_id, created_at
FROM workflow_stages WHERE workflow_id IN (%s)
ORDER BY ordinal ASC`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: workflow stages: %w", err)
}
defer rows.Close()
var out []models.WorkflowStage
for rows.Next() {
var ws models.WorkflowStage
if err := rows.Scan(&ws.ID, &ws.WorkflowID, &ws.Ordinal, &ws.Name,
&ws.PersonaID, &ws.AssignmentTeamID, &ws.FormTemplate,
&ws.StageMode, &ws.HistoryMode, &ws.AutoTransition,
&ws.TransitionRules, &ws.SurfacePkgID, st(&ws.CreatedAt)); err != nil {
return nil, fmt.Errorf("export: scan workflow stage: %w", err)
}
out = append(out, ws)
}
return out, rows.Err()
}
func (s *ExportStore) TeamGroups(ctx context.Context, teamID string) ([]models.Group, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, description, scope, team_id, created_by, source,
permissions, token_budget_daily, token_budget_monthly,
allowed_models, created_at, updated_at
FROM groups WHERE team_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team groups: %w", err)
}
defer rows.Close()
var out []models.Group
for rows.Next() {
var g models.Group
var permJSON, modelsJSON string
if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.Scope,
&g.TeamID, &g.CreatedBy, &g.Source,
&permJSON, &g.TokenBudgetDaily,
&g.TokenBudgetMonthly, &modelsJSON,
st(&g.CreatedAt), st(&g.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan group: %w", err)
}
g.Permissions = ScanArray(permJSON)
g.AllowedModels = ScanArray(modelsJSON)
out = append(out, g)
}
return out, rows.Err()
}
func (s *ExportStore) TeamGroupMembers(ctx context.Context, groupIDs []string) ([]models.GroupMember, error) {
if len(groupIDs) == 0 {
return nil, nil
}
ph, args := sqlitePlaceholders(groupIDs)
rows, err := DB.QueryContext(ctx, fmt.Sprintf(`
SELECT id, group_id, user_id, added_by, added_at
FROM group_members WHERE group_id IN (%s)`, ph), args...)
if err != nil {
return nil, fmt.Errorf("export: group members: %w", err)
}
defer rows.Close()
var out []models.GroupMember
for rows.Next() {
var gm models.GroupMember
if err := rows.Scan(&gm.ID, &gm.GroupID, &gm.UserID, &gm.AddedBy,
st(&gm.AddedAt)); err != nil {
return nil, fmt.Errorf("export: scan group member: %w", err)
}
out = append(out, gm)
}
return out, rows.Err()
}
func (s *ExportStore) TeamResourceGrants(ctx context.Context, teamID string) ([]models.ResourceGrant, error) {
rows, err := DB.QueryContext(ctx, `
SELECT rg.id, rg.resource_type, rg.resource_id, rg.grant_scope,
rg.granted_groups, rg.created_by, rg.created_at, rg.updated_at
FROM resource_grants rg
WHERE rg.resource_id IN (
SELECT id FROM personas WHERE scope = 'team' AND owner_id = ?
UNION ALL
SELECT id FROM knowledge_bases WHERE team_id = ?
)`, teamID, teamID)
if err != nil {
return nil, fmt.Errorf("export: team resource grants: %w", err)
}
defer rows.Close()
var out []models.ResourceGrant
for rows.Next() {
var rg models.ResourceGrant
var groupsJSON string
if err := rows.Scan(&rg.ID, &rg.ResourceType, &rg.ResourceID,
&rg.GrantScope, &groupsJSON,
&rg.CreatedBy, st(&rg.CreatedAt), st(&rg.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan resource grant: %w", err)
}
rg.GrantedGroups = ScanArray(groupsJSON)
out = append(out, rg)
}
return out, rows.Err()
}
func (s *ExportStore) TeamProjects(ctx context.Context, teamID string) ([]models.Project, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, name, description, color, icon, scope, owner_id,
team_id, is_archived, workspace_id, settings, created_at, updated_at
FROM projects WHERE team_id = ?
ORDER BY created_at ASC`, teamID)
if err != nil {
return nil, fmt.Errorf("export: team projects: %w", err)
}
defer rows.Close()
var out []models.Project
for rows.Next() {
var p models.Project
if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Color,
&p.Icon, &p.Scope, &p.OwnerID, &p.TeamID, &p.IsArchived,
&p.WorkspaceID, &p.Settings, st(&p.CreatedAt), st(&p.UpdatedAt)); err != nil {
return nil, fmt.Errorf("export: scan project: %w", err)
}
out = append(out, p)
}
return out, rows.Err()
}
// ── GDPR ─────────────────────────────────────
func (s *ExportStore) CountActiveAdmins(ctx context.Context) (int, error) {
var count int
err := DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE role = 'admin' AND is_active = 1`).Scan(&count)
return count, err
}
func (s *ExportStore) AnonymizeUser(ctx context.Context, userID string, anonHash string) error {
_, err := DB.ExecContext(ctx, `
UPDATE users SET
username = ?,
email = ?,
display_name = 'Deleted User',
avatar_url = '',
handle = ?,
settings = NULL,
external_id = NULL,
is_active = 0,
password_hash = '',
updated_at = datetime('now')
WHERE id = ?`,
"deleted-user-"+anonHash,
"deleted-"+anonHash+"@deleted.local",
"deleted-"+anonHash,
userID,
)
return err
}
func (s *ExportStore) SoftDeleteUserData(ctx context.Context, userID string) (map[string]int64, error) {
counts := make(map[string]int64)
tx, err := DB.BeginTx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("export: begin tx: %w", err)
}
defer tx.Rollback()
now := time.Now().UTC().Format(timeFmt)
// Messages in user-owned channels
r, err := tx.ExecContext(ctx, `
UPDATE messages SET deleted_at = ?
WHERE channel_id IN (SELECT id FROM channels WHERE user_id = ?)
AND deleted_at IS NULL`, now, userID)
if err != nil {
return nil, fmt.Errorf("export: soft-delete owned messages: %w", err)
}
n, _ := r.RowsAffected()
counts["messages_owned"] = n
// Messages sent by user in other channels
r, err = tx.ExecContext(ctx, `
UPDATE messages SET deleted_at = ?
WHERE participant_type = 'user' AND participant_id = ?
AND deleted_at IS NULL`, now, userID)
if err != nil {
return nil, fmt.Errorf("export: soft-delete sent messages: %w", err)
}
n, _ = r.RowsAffected()
counts["messages_sent"] = n
// Archive channels
r, err = tx.ExecContext(ctx, `UPDATE channels SET is_archived = 1 WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: archive channels: %w", err)
}
n, _ = r.RowsAffected()
counts["channels"] = n
// Notes + links
tx.ExecContext(ctx, `DELETE FROM note_links WHERE source_note_id IN (SELECT id FROM notes WHERE user_id = ?)`, userID)
r, err = tx.ExecContext(ctx, `DELETE FROM notes WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: delete notes: %w", err)
}
n, _ = r.RowsAffected()
counts["notes"] = n
// Memories
r, err = tx.ExecContext(ctx, `DELETE FROM memories WHERE owner_id = ? AND scope = 'user'`, userID)
if err != nil {
return nil, fmt.Errorf("export: delete memories: %w", err)
}
n, _ = r.RowsAffected()
counts["memories"] = n
// Workspace cascade
tx.ExecContext(ctx, `DELETE FROM workspace_chunks WHERE workspace_id IN (SELECT id FROM workspaces WHERE owner_type = 'user' AND owner_id = ?)`, userID)
tx.ExecContext(ctx, `DELETE FROM workspace_files WHERE workspace_id IN (SELECT id FROM workspaces WHERE owner_type = 'user' AND owner_id = ?)`, userID)
r, err = tx.ExecContext(ctx, `DELETE FROM workspaces WHERE owner_type = 'user' AND owner_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: delete workspaces: %w", err)
}
n, _ = r.RowsAffected()
counts["workspaces"] = n
// Files
r, err = tx.ExecContext(ctx, `DELETE FROM files WHERE user_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: delete files: %w", err)
}
n, _ = r.RowsAffected()
counts["files"] = n
// Projects cascade
tx.ExecContext(ctx, `DELETE FROM project_notes WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID)
tx.ExecContext(ctx, `DELETE FROM project_knowledge_bases WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID)
tx.ExecContext(ctx, `DELETE FROM project_channels WHERE project_id IN (SELECT id FROM projects WHERE scope = 'personal' AND owner_id = ?)`, userID)
r, err = tx.ExecContext(ctx, `DELETE FROM projects WHERE scope = 'personal' AND owner_id = ?`, userID)
if err != nil {
return nil, fmt.Errorf("export: delete projects: %w", err)
}
n, _ = r.RowsAffected()
counts["projects"] = n
// Folders, settings, prefs, notifications
tx.ExecContext(ctx, `DELETE FROM folders WHERE user_id = ?`, userID)
tx.ExecContext(ctx, `DELETE FROM user_model_settings WHERE user_id = ?`, userID)
tx.ExecContext(ctx, `DELETE FROM notification_preferences WHERE user_id = ?`, userID)
tx.ExecContext(ctx, `DELETE FROM notifications WHERE user_id = ?`, userID)
// Persona groups
tx.ExecContext(ctx, `DELETE FROM persona_group_members WHERE group_id IN (SELECT id FROM persona_groups WHERE owner_id = ?)`, userID)
tx.ExecContext(ctx, `DELETE FROM persona_groups WHERE owner_id = ?`, userID)
// Channel participation
tx.ExecContext(ctx, `DELETE FROM channel_participants WHERE participant_type = 'user' AND participant_id = ?`, userID)
tx.ExecContext(ctx, `DELETE FROM channel_cursors WHERE user_id = ?`, userID)
// Memory extraction log, presence
tx.ExecContext(ctx, `DELETE FROM memory_extraction_log WHERE user_id = ?`, userID)
tx.ExecContext(ctx, `DELETE FROM user_presence WHERE user_id = ?`, userID)
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("export: commit: %w", err)
}
return counts, nil
}
func (s *ExportStore) DeleteUserTokens(ctx context.Context, userID string) error {
_, err := DB.ExecContext(ctx, `DELETE FROM refresh_tokens WHERE user_id = ?`, userID)
if err != nil {
return fmt.Errorf("export: delete refresh tokens: %w", err)
}
_, err = DB.ExecContext(ctx, `DELETE FROM ws_tickets WHERE user_id = ?`, userID)
if err != nil {
return fmt.Errorf("export: delete ws tickets: %w", err)
}
return nil
}
// ── Import (CS1) ─────────────────────────────
func (s *ExportStore) ImportFolders(ctx context.Context, folders []models.Folder) (int, int, error) {
imported, skipped := 0, 0
for _, f := range folders {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO folders (id, user_id, name, parent_id, sort_order, created_at, updated_at)
VALUES (?,?,?,?,?,?,?)`,
f.ID, f.UserID, f.Name, f.ParentID, f.SortOrder,
f.CreatedAt.Format(timeFmt), f.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import folder %s: %w", f.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportProjects(ctx context.Context, projects []models.Project) (int, int, error) {
imported, skipped := 0, 0
for _, p := range projects {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO projects (id, name, description, color, icon, scope, owner_id,
team_id, is_archived, workspace_id, settings, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
p.ID, p.Name, p.Description, p.Color, p.Icon, p.Scope, p.OwnerID,
p.TeamID, p.IsArchived, p.WorkspaceID, p.Settings,
p.CreatedAt.Format(timeFmt), p.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import project %s: %w", p.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportWorkspaces(ctx context.Context, workspaces []models.Workspace) (int, int, error) {
imported, skipped := 0, 0
for _, w := range workspaces {
var gitLastSync interface{}
if w.GitLastSync != nil {
t := w.GitLastSync.Format(timeFmt)
gitLastSync = t
}
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO workspaces (id, owner_type, owner_id, name, root_path, max_bytes, status,
indexing_enabled, git_remote_url, git_branch, git_credential_id,
git_last_sync, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
w.ID, w.OwnerType, w.OwnerID, w.Name, w.RootPath, w.MaxBytes, w.Status,
w.IndexingEnabled, w.GitRemoteURL, w.GitBranch, w.GitCredentialID,
gitLastSync, w.CreatedAt.Format(timeFmt), w.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import workspace %s: %w", w.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportChannels(ctx context.Context, channels []models.Channel) (int, int, error) {
imported, skipped := 0, 0
for _, ch := range channels {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO channels (id, user_id, title, description, type, model, system_prompt,
provider_config_id, is_archived, is_pinned, folder_id,
team_id, project_id, workspace_id, settings, allow_anonymous,
created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
ch.ID, ch.UserID, ch.Title, ch.Description, ch.Type, ch.Model, ch.SystemPrompt,
ch.ProviderConfigID, ch.IsArchived, ch.IsPinned, ch.FolderID, ch.TeamID,
ch.ProjectID, ch.WorkspaceID, ch.Settings, ch.AllowAnonymous,
ch.CreatedAt.Format(timeFmt), ch.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import channel %s: %w", ch.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportChannelParticipants(ctx context.Context, cps []models.ChannelParticipant) (int, int, error) {
imported, skipped := 0, 0
for _, cp := range cps {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO channel_participants (id, channel_id, participant_type, participant_id, role,
display_name, avatar_url, joined_at, last_read_at)
VALUES (?,?,?,?,?,?,?,?,?)`,
cp.ID, cp.ChannelID, cp.ParticipantType, cp.ParticipantID, cp.Role,
cp.DisplayName, cp.AvatarURL,
cp.JoinedAt.Format(timeFmt), cp.LastReadAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import participant %s: %w", cp.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportChannelModels(ctx context.Context, cms []models.ChannelModel) (int, int, error) {
imported, skipped := 0, 0
for _, cm := range cms {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO channel_models (id, channel_id, model_id, provider_config_id, persona_id,
handle, display_name, system_prompt, is_default)
VALUES (?,?,?,?,?,?,?,?,?)`,
cm.ID, cm.ChannelID, cm.ModelID, cm.ProviderConfigID, cm.PersonaID,
cm.Handle, cm.DisplayName, cm.SystemPrompt, cm.IsDefault)
if err != nil {
return imported, skipped, fmt.Errorf("import channel model %s: %w", cm.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportChannelCursors(ctx context.Context, cursors []models.ChannelCursor) (int, int, error) {
imported, skipped := 0, 0
for _, cc := range cursors {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO channel_cursors (id, channel_id, user_id, active_leaf_id, updated_at)
VALUES (?,?,?,?,?)`,
cc.ID, cc.ChannelID, cc.UserID, cc.ActiveLeafID,
cc.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import cursor %s: %w", cc.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportMessages(ctx context.Context, msgs []models.Message) (int, int, error) {
imported, skipped := 0, 0
for _, m := range msgs {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO messages (id, channel_id, role, content, model, tokens_used,
tool_calls, metadata, parent_id, sibling_index,
participant_type, participant_id, provider_config_id,
created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
m.ID, m.ChannelID, m.Role, m.Content, m.Model, m.TokensUsed,
ToJSON(m.ToolCalls), ToJSON(m.Metadata), m.ParentID, m.SiblingIndex,
m.ParticipantType, m.ParticipantID, m.ProviderConfigID,
m.CreatedAt.Format(timeFmt), m.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import message %s: %w", m.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportNotes(ctx context.Context, notes []models.Note) (int, int, error) {
imported, skipped := 0, 0
for _, n := range notes {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO notes (id, user_id, title, content, folder_path, tags,
metadata, source_channel_id, source_message_id, team_id,
created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
n.ID, n.UserID, n.Title, n.Content, n.FolderPath, ArrayToJSON(n.Tags),
ToJSON(n.Metadata), models.NullString(n.SourceChannelID),
models.NullString(n.SourceMessageID), models.NullString(n.TeamID),
n.CreatedAt.Format(timeFmt), n.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import note %s: %w", n.ID, err)
}
nr, _ := r.RowsAffected()
if nr > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportNoteLinks(ctx context.Context, links []models.ExportNoteLink) (int, int, error) {
imported, skipped := 0, 0
for _, nl := range links {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO note_links (source_note_id, target_note_id, target_title, display_text, is_transclusion)
VALUES (?,?,?,?,?)`,
nl.SourceNoteID, nl.TargetNoteID, nl.TargetTitle, nl.DisplayText, nl.IsTransclusion)
if err != nil {
return imported, skipped, fmt.Errorf("import note link: %w", err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportMemories(ctx context.Context, mems []models.Memory) (int, int, error) {
imported, skipped := 0, 0
for _, m := range mems {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO memories (id, scope, owner_id, user_id, key, value,
source_channel_id, confidence, status, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
m.ID, m.Scope, m.OwnerID, m.UserID, m.Key, m.Value,
m.SourceChannelID, m.Confidence, m.Status,
m.CreatedAt.Format(timeFmt), m.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import memory %s: %w", m.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportProjectChannels(ctx context.Context, pcs []models.ProjectChannel) (int, int, error) {
imported, skipped := 0, 0
for _, pc := range pcs {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO project_channels (project_id, channel_id, position, folder, added_at)
VALUES (?,?,?,?,?)`,
pc.ProjectID, pc.ChannelID, pc.Position, pc.Folder, pc.AddedAt)
if err != nil {
return imported, skipped, fmt.Errorf("import project channel: %w", err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportProjectKBs(ctx context.Context, pks []models.ProjectKB) (int, int, error) {
imported, skipped := 0, 0
for _, pk := range pks {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO project_knowledge_bases (project_id, kb_id, auto_search, added_at)
VALUES (?,?,?,?)`,
pk.ProjectID, pk.KBID, pk.AutoSearch, pk.AddedAt)
if err != nil {
return imported, skipped, fmt.Errorf("import project KB: %w", err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportProjectNotes(ctx context.Context, pns []models.ProjectNote) (int, int, error) {
imported, skipped := 0, 0
for _, pn := range pns {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO project_notes (project_id, note_id, added_at)
VALUES (?,?,?)`,
pn.ProjectID, pn.NoteID, pn.AddedAt)
if err != nil {
return imported, skipped, fmt.Errorf("import project note: %w", err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportWorkspaceFiles(ctx context.Context, wfs []models.WorkspaceFile) (int, int, error) {
imported, skipped := 0, 0
for _, wf := range wfs {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO workspace_files (id, workspace_id, path, is_directory, content_type,
size_bytes, sha256, index_status, chunk_count, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
wf.ID, wf.WorkspaceID, wf.Path, wf.IsDirectory, wf.ContentType,
wf.SizeBytes, wf.SHA256, wf.IndexStatus, wf.ChunkCount,
wf.CreatedAt.Format(timeFmt), wf.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import workspace file %s: %w", wf.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportFiles(ctx context.Context, files []models.File) (int, int, error) {
imported, skipped := 0, 0
for _, f := range files {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO files (id, channel_id, message_id, user_id, project_id,
origin, filename, content_type, size_bytes, storage_key,
display_hint, extracted_text, metadata, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
f.ID, f.ChannelID, f.MessageID, f.UserID, f.ProjectID,
f.Origin, f.Filename, f.ContentType, f.SizeBytes, f.StorageKey,
f.DisplayHint, f.ExtractedText, f.Metadata,
f.CreatedAt.Format(timeFmt), f.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import file %s: %w", f.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportUserModelSettings(ctx context.Context, ums []models.UserModelSetting) (int, int, error) {
imported, skipped := 0, 0
for _, um := range ums {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO user_model_settings (id, user_id, model_id, provider_config_id, hidden,
preferred_temperature, preferred_max_tokens, sort_order, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)`,
um.ID, um.UserID, um.ModelID, um.ProviderConfigID, um.Hidden,
um.PreferredTemperature, um.PreferredMaxTokens, um.SortOrder,
um.CreatedAt.Format(timeFmt), um.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import user model setting %s: %w", um.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportNotifPrefs(ctx context.Context, nps []models.NotificationPreference) (int, int, error) {
imported, skipped := 0, 0
for _, np := range nps {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO notification_preferences (id, user_id, type, in_app, email)
VALUES (?,?,?,?,?)`,
np.ID, np.UserID, np.Type, np.InApp, np.Email)
if err != nil {
return imported, skipped, fmt.Errorf("import notif pref %s: %w", np.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportPersonaGroups(ctx context.Context, pgs []models.PersonaGroup) (int, int, error) {
imported, skipped := 0, 0
for _, pg := range pgs {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO persona_groups (id, name, description, owner_id, scope, team_id,
created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?)`,
pg.ID, pg.Name, pg.Description, pg.OwnerID, pg.Scope, pg.TeamID,
pg.CreatedAt.Format(timeFmt), pg.UpdatedAt.Format(timeFmt))
if err != nil {
return imported, skipped, fmt.Errorf("import persona group %s: %w", pg.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}
func (s *ExportStore) ImportPersonaGroupMembers(ctx context.Context, pgms []models.PersonaGroupMember) (int, int, error) {
imported, skipped := 0, 0
for _, pgm := range pgms {
r, err := DB.ExecContext(ctx, `
INSERT OR IGNORE INTO persona_group_members (id, group_id, persona_id, is_leader, sort_order)
VALUES (?,?,?,?,?)`,
pgm.ID, pgm.GroupID, pgm.PersonaID, pgm.IsLeader, pgm.SortOrder)
if err != nil {
return imported, skipped, fmt.Errorf("import persona group member %s: %w", pgm.ID, err)
}
n, _ := r.RowsAffected()
if n > 0 {
imported++
} else {
skipped++
}
}
return imported, skipped, nil
}