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

1450 lines
64 KiB
Go

package store
import (
"context"
"encoding/json"
"errors"
"time"
"switchboard-core/models"
)
// ── Sentinel Errors ─────────────────────────
// ErrSystemGroup is returned when attempting to delete a system-sourced group.
var ErrSystemGroup = errors.New("system groups cannot be deleted")
// =========================================
// STORES — Data Access Layer
// =========================================
// Every database operation goes through these interfaces.
// Handlers never touch SQL directly. This makes the DB
// portable (Postgres today, SQLite/MySQL later) and
// handlers testable with in-memory implementations.
// =========================================
// Stores bundles all store interfaces for dependency injection.
type Stores struct {
Providers ProviderStore
Catalog CatalogStore
Personas PersonaStore
Policies PolicyStore
UserSettings UserModelSettingsStore
Users UserStore
Teams TeamStore
Channels ChannelStore
Messages MessageStore
Audit AuditStore
Notes NoteStore
NoteLinks NoteLinkStore
GlobalConfig GlobalConfigStore
Usage UsageStore
Pricing PricingStore
Files FileStore
KnowledgeBases KnowledgeBaseStore
Groups GroupStore
ResourceGrants ResourceGrantStore
Memories MemoryStore
Projects ProjectStore
Notifications NotificationStore
NotifPrefs NotificationPreferenceStore
Workspaces WorkspaceStore
GitCredentials GitCredentialStore
CapOverrides CapabilityOverrideStore
RoutingPolicies RoutingPolicyStore
Sessions SessionStore
Packages PackageStore // v0.28.7: Unified package registry (surfaces + extensions)
Workflows WorkflowStore // v0.26.1: Workflow definitions + stages
Tasks TaskStore // v0.27.1: Task scheduling + run history
Presence PresenceStore // v0.29.0: User online/offline status
PersonaGroups PersonaGroupStore // v0.29.0: Persona group (roster template) CRUD
Folders FolderStore // v0.29.0: Chat folder CRUD
Health HealthStore // v0.29.0: Provider health window management
ExtPermissions ExtensionPermissionStore // v0.29.0: Extension declared/granted capabilities
ExtData ExtDataStore // v0.29.2: Extension namespaced table catalog
Tickets TicketStore // v0.32.0: WS auth tickets (PG-backed for cross-pod)
RateLimits RateLimitStore // v0.32.0: Distributed rate limiting
Export ExportStore // v0.34.0: Data portability batch reads + GDPR ops
Connections ConnectionStore // v0.38.1: Extension connection credentials
Dependencies DependencyStore // v0.38.2: Library package dependency graph
}
// =========================================
// PROVIDER STORE
// =========================================
type ProviderStore interface {
Create(ctx context.Context, cfg *models.ProviderConfig) error
GetByID(ctx context.Context, id string) (*models.ProviderConfig, error)
Update(ctx context.Context, id string, patch models.ProviderConfigPatch) error
Delete(ctx context.Context, id string) error
// Scoped queries
ListGlobal(ctx context.Context) ([]models.ProviderConfig, error)
ListForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error)
ListForUser(ctx context.Context, userID string) ([]models.ProviderConfig, error) // personal scope
ListAccessible(ctx context.Context, userID string) ([]models.ProviderConfig, error) // all user can access
// Access check
UserCanAccess(ctx context.Context, userID, configID string) (bool, error)
// ── CS4 additions (v0.29.0) ──
// DeletePersonalByOwner removes all personal-scope provider configs for a user.
DeletePersonalByOwner(ctx context.Context, ownerID string) (int64, error)
// ── CS6 additions (v0.29.0) ──
// ListAllForTeam returns all provider configs (including inactive) for a team.
ListAllForTeam(ctx context.Context, teamID string) ([]models.ProviderConfig, error)
// DeleteByIDAndTeam deletes a team-scoped provider by ID and team. Returns rows affected.
DeleteByIDAndTeam(ctx context.Context, id, teamID string) (int64, error)
// ── CS7a additions (v0.29.0) ──
// FindFirstForUser returns the ID of the first active provider config
// accessible to a user (personal first, then global; excludes team).
// Returns sql.ErrNoRows if none found.
FindFirstForUser(ctx context.Context, userID string) (string, error)
// LoadAccessible loads a provider config by ID, verifying the user has access
// (global, personal owned by user, or team the user belongs to).
// Returns sql.ErrNoRows if not found or not accessible.
LoadAccessible(ctx context.Context, configID, userID string) (*models.ProviderConfig, error)
}
// =========================================
// CATALOG STORE
// =========================================
type CatalogStore interface {
// Sync from provider API
UpsertFromSync(ctx context.Context, providerConfigID string, entries []CatalogSyncEntry) (added, updated int, err error)
// Queries
GetByID(ctx context.Context, id string) (*models.CatalogEntry, error)
GetByModelID(ctx context.Context, providerConfigID, modelID string) (*models.CatalogEntry, error)
GetByModelIDAny(ctx context.Context, modelID string) (*models.CatalogEntry, error) // any provider, most recently synced
ListVisible(ctx context.Context) ([]models.CatalogEntry, error)
ListEnabledForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error)
ListForProvider(ctx context.Context, providerConfigID string) ([]models.CatalogEntry, error)
ListAll(ctx context.Context) ([]models.CatalogEntry, error) // everything (internal use)
ListAllGlobal(ctx context.Context) ([]models.CatalogEntry, error) // admin view — global-scope providers only
// Visibility management
SetVisibility(ctx context.Context, id string, visibility string) error
BulkSetVisibility(ctx context.Context, providerConfigID string, visibility string) error
BulkSetVisibilityAll(ctx context.Context, visibility string) error
// Delete
Delete(ctx context.Context, id string) error
DeleteForProvider(ctx context.Context, providerConfigID string) error
// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──
// FindEnabledByModelID finds an enabled catalog entry by exact model_id
// (case-insensitive). Returns (modelID, providerConfigID, nil).
// Prefers global > team > personal scope providers.
FindEnabledByModelID(ctx context.Context, modelID string) (string, string, error)
// FindEnabledByModelIDPrefix finds by unambiguous model_id prefix.
// Returns (modelID, configID, matchCount, nil).
FindEnabledByModelIDPrefix(ctx context.Context, prefix string) (string, string, int, error)
// ── CS2 additions (v0.29.0) ──
// GetCapabilities returns the raw capabilities JSON for a specific provider+model.
GetCapabilities(ctx context.Context, modelID, configID string) ([]byte, error)
// GetCapabilitiesAny returns capabilities for a model from any provider (most recent sync).
GetCapabilitiesAny(ctx context.Context, modelID string) ([]byte, error)
// ── CS6 additions (v0.29.0) ──
// ListTeamAvailable returns catalog entries with visibility 'enabled' or 'team'
// from active global providers. Used by team model selector.
ListTeamAvailable(ctx context.Context) ([]TeamAvailableModel, error)
}
// TeamAvailableModel is returned by CatalogStore.ListTeamAvailable.
type TeamAvailableModel struct {
ID string
ModelID string
DisplayName *string
Visibility string
Provider string
ProviderName string
}
// CatalogSyncEntry is the input format from provider FetchModels.
type CatalogSyncEntry struct {
ModelID string
DisplayName string
ModelType string // "chat", "embedding", "image" — from provider API
Capabilities models.ModelCapabilities
Pricing *models.ModelPricing
}
// =========================================
// PERSONA STORE
// =========================================
type PersonaStore interface {
Create(ctx context.Context, p *models.Persona) error
GetByID(ctx context.Context, id string) (*models.Persona, error)
Update(ctx context.Context, id string, patch models.PersonaPatch) error
Delete(ctx context.Context, id string) error
// Scoped queries
ListForUser(ctx context.Context, userID string) ([]models.Persona, error) // all visible to user
ListForTeam(ctx context.Context, teamID string) ([]models.Persona, error)
ListGlobal(ctx context.Context) ([]models.Persona, error)
// Grants
SetGrants(ctx context.Context, personaID string, grants []models.Grant) error
GetGrants(ctx context.Context, personaID string) ([]models.Grant, error)
GetToolGrants(ctx context.Context, personaID string) ([]string, error)
// Access check
UserCanAccess(ctx context.Context, userID, personaID string) (bool, error)
// Knowledge base bindings
SetKBs(ctx context.Context, personaID string, kbIDs []string, autoSearch map[string]bool) error
GetKBs(ctx context.Context, personaID string) ([]models.PersonaKB, error)
GetKBIDs(ctx context.Context, personaID string) ([]string, error)
// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──
// FindActiveByHandle returns an active persona's ID by exact handle match (case-insensitive).
// Returns "" if not found.
FindActiveByHandle(ctx context.Context, handle string) (string, error)
// FindActiveByHandlePrefix returns a persona ID by unambiguous handle prefix.
// Returns ("", count, nil) if ambiguous or no match.
FindActiveByHandlePrefix(ctx context.Context, prefix string) (string, int, error)
// GetNameByID returns the persona's display name. Returns "" if not found.
GetNameByID(ctx context.Context, id string) (string, error)
// GetNamesByIDs returns a map of persona ID → name for batch resolution.
GetNamesByIDs(ctx context.Context, ids []string) (map[string]string, error)
// GetDisplayInfoByIDs returns name + avatar for batch sender resolution.
GetDisplayInfoByIDs(ctx context.Context, ids []string) (map[string]UserDisplayInfo, error)
}
// =========================================
// POLICY STORE
// =========================================
type PolicyStore interface {
Get(ctx context.Context, key string) (string, error)
GetBool(ctx context.Context, key string) (bool, error)
Set(ctx context.Context, key, value string, updatedBy string) error
GetAll(ctx context.Context) (map[string]string, error)
}
// =========================================
// USER MODEL SETTINGS STORE
// =========================================
type UserModelSettingsStore interface {
GetForUser(ctx context.Context, userID string) ([]models.UserModelSetting, error)
GetHiddenModelIDs(ctx context.Context, userID string) (map[string]bool, error)
Set(ctx context.Context, userID, modelID string, providerConfigID *string, patch models.UserModelSettingPatch) error
BulkSetHidden(ctx context.Context, userID string, entries []models.HiddenEntry, hidden bool) error
}
// =========================================
// USER STORE
// =========================================
type UserStore interface {
Create(ctx context.Context, u *models.User) error
GetByID(ctx context.Context, id string) (*models.User, error)
GetByUsername(ctx context.Context, username string) (*models.User, error)
GetByEmail(ctx context.Context, email string) (*models.User, error)
GetByLogin(ctx context.Context, login string) (*models.User, error) // username or email
GetByHandle(ctx context.Context, handle string) (*models.User, error)
GetByExternalID(ctx context.Context, authSource, externalID string) (*models.User, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, opts ListOptions) ([]models.User, int, error)
UpdateLastLogin(ctx context.Context, id string) error
SetActive(ctx context.Context, id string, active bool) error
ListActiveUserIDs(ctx context.Context) ([]string, error)
// Refresh tokens
CreateRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error
GetRefreshToken(ctx context.Context, tokenHash string) (userID string, err error)
RevokeRefreshToken(ctx context.Context, tokenHash string) error
RevokeAllRefreshTokens(ctx context.Context, userID string) error
CleanExpiredTokens(ctx context.Context) error
// ── Mention resolution (v0.29.0 — moved from handler raw SQL) ──
// FindActiveByHandle returns a user ID by exact handle match (case-insensitive).
// excludeUserID prevents self-mention resolution.
FindActiveByHandle(ctx context.Context, handle, excludeUserID string) (string, error)
// FindActiveByHandlePrefix returns a user ID by unambiguous handle prefix.
// Returns ("", count, nil) if ambiguous or no match.
FindActiveByHandlePrefix(ctx context.Context, prefix, excludeUserID string) (string, int, error)
// GetDisplayInfoByIDs returns name + avatar for batch sender resolution.
GetDisplayInfoByIDs(ctx context.Context, ids []string) (map[string]UserDisplayInfo, error)
// ── CS1 additions (v0.29.0) ──
// Exists returns true if a user with the given ID exists and is active.
Exists(ctx context.Context, userID string) (bool, error)
// SearchActive returns users matching a query (username, display_name, handle).
// Excludes the calling user. Max 20 results.
SearchActive(ctx context.Context, excludeUserID, query string) ([]UserSearchResult, error)
// ── CS2 additions (v0.29.0) ──
// CountByRole returns the number of users with a given role.
CountByRole(ctx context.Context, role string) (int, error)
// CountAll returns the total number of users.
CountAll(ctx context.Context) (int, error)
// MergeSettings performs a dialect-aware JSON merge into the user's settings column.
MergeSettings(ctx context.Context, userID string, patch []byte) error
// GetVaultKeys returns the user's vault encryption state.
GetVaultKeys(ctx context.Context, userID string) (vaultSet bool, encUEK, salt, nonce []byte, err error)
// UpdateVaultKeys re-wraps the user's vault keys (password change).
UpdateVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
// ── CS4 additions (v0.29.0) ──
// ClearVaultKeys nulls out vault columns and sets vault_set = false.
ClearVaultKeys(ctx context.Context, userID string) error
// InitVaultKeys stores vault keys and sets vault_set = true (first-time init).
InitVaultKeys(ctx context.Context, userID string, encUEK, salt, nonce []byte) error
}
// UserSearchResult is a lightweight result for user search.
type UserSearchResult struct {
ID string `json:"id"`
Username string `json:"username"`
DisplayName string `json:"display_name"`
Handle string `json:"handle"`
}
// =========================================
// TEAM STORE
// =========================================
type TeamStore interface {
Create(ctx context.Context, t *models.Team) error
GetByID(ctx context.Context, id string) (*models.Team, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]models.Team, error)
ListForUser(ctx context.Context, userID string) ([]models.Team, error) // active teams with MyRole
// Members
AddMember(ctx context.Context, teamID, userID, role string) error
RemoveMember(ctx context.Context, teamID, userID string) error
UpdateMemberRole(ctx context.Context, teamID, userID, role string) error
ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error)
GetMember(ctx context.Context, teamID, userID string) (*models.TeamMember, error)
GetUserTeamIDs(ctx context.Context, userID string) ([]string, error)
IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error)
IsMember(ctx context.Context, teamID, userID string) (bool, error)
// ── CS1 additions (v0.29.0) ──
// Exists returns true if a team with the given ID exists.
Exists(ctx context.Context, teamID string) (bool, error)
// UpdateMemberRoleByID updates a member's role using the member row ID.
UpdateMemberRoleByID(ctx context.Context, memberID, teamID, role string) (int64, error)
// DeleteMemberByID deletes a member using the member row ID.
DeleteMemberByID(ctx context.Context, memberID, teamID string) (int64, error)
// ListTeamAuditActions returns distinct audit actions for a team's members.
ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error)
// ── CS5b additions (v0.29.0) ──
// GetFirstTeamIDForUser returns the first team_id the user belongs to, or "" if none.
GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error)
// ── CS6 additions (v0.29.0) ──
// AddMemberReturningID inserts a team member and returns the row ID.
AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error)
// HasPrivateProviderRequirement checks if a user belongs to any active team
// with the require_private_providers setting enabled.
HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error)
// MergeSettings merges a JSON string into the team's settings column.
MergeSettings(ctx context.Context, teamID, settingsJSON string) error
}
// =========================================
// CHANNEL STORE
// =========================================
type ChannelStore interface {
Create(ctx context.Context, ch *models.Channel) error
GetByID(ctx context.Context, id string) (*models.Channel, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error
ListForUser(ctx context.Context, userID string, opts ListOptions) ([]models.Channel, int, error)
Search(ctx context.Context, userID, query string, opts ListOptions) ([]models.Channel, int, error)
// Cursor management (conversation forking)
GetCursor(ctx context.Context, channelID, userID string) (*models.ChannelCursor, error)
SetCursor(ctx context.Context, channelID, userID, leafID string) error
// Channel models
SetModel(ctx context.Context, cm *models.ChannelModel) error
GetModels(ctx context.Context, channelID string) ([]models.ChannelModel, error)
GetModelByID(ctx context.Context, id string) (*models.ChannelModel, error)
UpdateModel(ctx context.Context, id string, fields map[string]interface{}) error
DeleteModel(ctx context.Context, id string) error
// Ownership check
UserOwns(ctx context.Context, channelID, userID string) (bool, error)
// Channel participants (ICD §3.7)
AddParticipant(ctx context.Context, p *models.ChannelParticipant) error
ListParticipants(ctx context.Context, channelID string) ([]models.ChannelParticipant, error)
GetParticipantByID(ctx context.Context, id string) (*models.ChannelParticipant, error)
UpdateParticipantRole(ctx context.Context, id, role string) error
RemoveParticipant(ctx context.Context, id string) error
IsParticipant(ctx context.Context, channelID, pType, pID string) (bool, error)
GetParticipantRole(ctx context.Context, channelID, pType, pID string) (string, error)
CountParticipantsByRole(ctx context.Context, channelID, role string) (int, error)
// Remove model roster entries auto-created by a persona participant
DeleteModelByPersona(ctx context.Context, channelID, personaID string) error
// Workspace resolution: channel workspace_id > project workspace_id
ResolveWorkspaceID(ctx context.Context, channelID string) (string, error)
// Admin: archived channel management (v0.23.2)
ListArchived(ctx context.Context, opts ListOptions) ([]ArchivedChannel, int, error)
Purge(ctx context.Context, id string) error // hard-delete; channel must be archived
// ── Single-field helpers (v0.29.0 — moved from handler raw SQL) ──
// GetAIMode returns the channel's ai_mode ("auto", "off", "mention_only").
GetAIMode(ctx context.Context, channelID string) (string, error)
// GetTypeAndTeamID returns (channel_type, team_id) in one query.
GetTypeAndTeamID(ctx context.Context, channelID string) (string, *string, error)
// GetSystemPrompt returns the channel's system_prompt (may be nil).
GetSystemPrompt(ctx context.Context, channelID string) (*string, error)
// GetDefaultModel returns the channel's model field (may be nil).
GetDefaultModel(ctx context.Context, channelID string) (*string, error)
// TouchUpdatedAt bumps the channel's updated_at to now.
TouchUpdatedAt(ctx context.Context, channelID string) error
// ListUserParticipantIDs returns user participant IDs, excluding excludeUserID.
ListUserParticipantIDs(ctx context.Context, channelID, excludeUserID string) ([]string, error)
// ListPersonaParticipantIDs returns persona participant IDs ordered by created_at.
ListPersonaParticipantIDs(ctx context.Context, channelID string) ([]string, error)
// GetLeaderPersonaID returns the persona group leader for a channel.
// Falls back to the first persona participant if no leader flag is set.
// Returns empty string if no persona participants exist.
GetLeaderPersonaID(ctx context.Context, channelID string) (string, error)
// GetWorkflowInfo returns (workflow_id, current_stage) for workflow-type channels.
// Returns nil workflow_id if the channel is not a workflow channel.
GetWorkflowInfo(ctx context.Context, channelID string) (*string, int, error)
// ── DM + lifecycle helpers (v0.29.0-cs1) ──
// FindExistingDM returns the channel ID of an existing DM between two users.
// Returns "" if no DM exists.
FindExistingDM(ctx context.Context, userID1, userID2 string) (string, error)
// GetUnreadCount returns the number of unread messages for a user in a channel.
GetUnreadCount(ctx context.Context, channelID, userID string) (int, error)
// DeleteByOwner deletes a channel if the user is the owner.
// Returns rows affected (0 = not found or not owner).
DeleteByOwner(ctx context.Context, channelID, userID string) (int64, error)
// ArchiveForRetention archives a channel and sets a purge_after timestamp.
ArchiveForRetention(ctx context.Context, channelID string, purgeAfter time.Time) error
// ListPurgeable returns IDs of channels whose purge_after has passed.
ListPurgeable(ctx context.Context) ([]string, error)
// MarkRead updates last_read_at and last_read_message_id for a user.
MarkRead(ctx context.Context, channelID, userID string) error
// CountParticipantsByType returns count of participants of a given type.
CountParticipantsByType(ctx context.Context, channelID, pType string) (int, error)
// CountAll returns the total number of channels.
CountAll(ctx context.Context) (int, error)
// ── Workflow instance state (v0.29.0-cs3) ──
// SetWorkflowInstance initializes workflow columns on a channel.
SetWorkflowInstance(ctx context.Context, channelID, workflowID string, version int, stageData json.RawMessage, status string) error
// GetWorkflowStatus returns the full workflow state for a workflow channel.
GetWorkflowStatus(ctx context.Context, channelID string) (*WorkflowChannelStatus, error)
// AdvanceWorkflowStage updates current_stage + stage_data + last_activity_at.
AdvanceWorkflowStage(ctx context.Context, channelID string, nextStage int, stageData json.RawMessage) error
// CompleteWorkflow sets workflow_status='completed' and ai_mode='off'.
CompleteWorkflow(ctx context.Context, channelID string, finalStage int, stageData json.RawMessage) error
// CancelWorkflow sets workflow_status='cancelled' and ai_mode='off'. (v0.37.15)
CancelWorkflow(ctx context.Context, channelID string) error
// RejectWorkflowToStage resets current_stage (no stage_data change).
RejectWorkflowToStage(ctx context.Context, channelID string, stage int) error
// GetStageData returns the current stage_data JSON from a channel.
GetStageData(ctx context.Context, channelID string) (json.RawMessage, error)
// ── Background job helpers (v0.29.0-cs4) ──
// MarkStaleWorkflows sets workflow_status='stale' on active workflow channels
// with last_activity_at before cutoff. Returns rows affected.
MarkStaleWorkflows(ctx context.Context, cutoff time.Time) (int64, error)
// EnforceWorkflowRetention deletes completed workflow channels whose parent
// workflow has retention.mode="delete" and the retention period has elapsed.
// PG-only (uses JSON operators). Returns rows affected. No-op on SQLite.
EnforceWorkflowRetention(ctx context.Context) (int64, error)
// GetTypeAndAllowAnonymous returns (type, allow_anonymous) for session auth.
GetTypeAndAllowAnonymous(ctx context.Context, channelID string) (string, bool, error)
// ── CS5b additions (v0.29.0) ──
// FindCompactionCandidates returns direct channels eligible for auto-compaction.
// Filters by activity gap, max age, minimum message count, and minimum total chars.
// PG-only (uses settings::text cast). Returns empty slice on SQLite.
FindCompactionCandidates(ctx context.Context, activityBefore, createdAfter time.Time, minMessages, minChars, limit int) ([]models.Channel, error)
// ── CS7a additions (v0.29.0) ──
// GetProviderConfigID returns the channel's provider_config_id (may be nil).
GetProviderConfigID(ctx context.Context, channelID string) (*string, error)
// UserCanAccess checks if a user is the channel owner or a participant.
UserCanAccess(ctx context.Context, channelID, userID string) (bool, error)
// ── CS7b additions (v0.29.0) ──
// ListFiltered returns paginated channels with message counts, applying
// complex filters (types, folder, search, project). Includes channels
// owned by the user or where the user is a participant.
ListFiltered(ctx context.Context, userID string, filter ChannelListFilter) ([]ChannelListItem, int, error)
// GetForUser loads a single channel with message count, verifying the
// user is the owner or a participant. Returns sql.ErrNoRows if not found.
GetForUser(ctx context.Context, channelID, userID string) (*ChannelListItem, error)
// CreateFull atomically creates a channel, adds the owner as a participant,
// optionally adds DM partner participants, and creates a default channel_model.
// The channel model (ch) is populated with ID, CreatedAt, UpdatedAt on return.
// folder, tags, and aiMode are passed separately as they're not in models.Channel.
CreateFull(ctx context.Context, ch *models.Channel, folder string, tags []string, aiMode string,
ownerUserID string, dmPartnerIDs []string, defaultModel, defaultConfigID string) error
// MergeSettings merges a JSON object into the channel's settings column.
MergeSettings(ctx context.Context, channelID string, settingsJSON json.RawMessage) error
// ── Monitoring (v0.35.0) ──
// ListByType returns all channels of a given type (e.g. "workflow").
ListByType(ctx context.Context, channelType string) ([]models.Channel, error)
}
// ChannelListFilter holds filter options for ChannelStore.ListFiltered.
type ChannelListFilter struct {
ListOptions
Archived bool
Types []string // empty = all
Folder string
FolderID string
Search string // title ILIKE/LIKE
ProjectID string // "none" = NULL, uuid = specific, "" = no filter
}
// ChannelListItem is returned by ListFiltered and GetForUser.
type ChannelListItem struct {
ID string
UserID string
Title string
Type string
AiMode string
Topic *string
Description *string
Model *string
ProviderConfigID *string
SystemPrompt *string
IsArchived bool
IsPinned bool
Folder *string
FolderID *string
ProjectID *string
WorkspaceID *string
Tags []string
Settings json.RawMessage
MessageCount int
UnreadCount int
CreatedAt string
UpdatedAt string
CreatedAtTime time.Time `json:"-"` // SQLite scan target
UpdatedAtTime time.Time `json:"-"` // SQLite scan target
}
// ArchivedChannel is a view model for the admin archived channels list.
type ArchivedChannel struct {
ID string `json:"id"`
Title string `json:"title"`
Type string `json:"type"`
OwnerName string `json:"owner_name"`
UpdatedAt string `json:"updated_at"`
MessageCount int `json:"message_count"`
}
// =========================================
// MESSAGE STORE
// =========================================
type MessageStore interface {
Create(ctx context.Context, m *models.Message) error
GetByID(ctx context.Context, id string) (*models.Message, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error // soft delete
ListForChannel(ctx context.Context, channelID string, opts ListOptions) ([]models.Message, error)
// Tree operations
GetChildren(ctx context.Context, parentID string) ([]models.Message, error)
GetSiblings(ctx context.Context, messageID string) ([]models.Message, error)
GetPathToRoot(ctx context.Context, messageID string) ([]models.Message, error)
GetNextSiblingIndex(ctx context.Context, parentID string) (int, error)
// Count
CountForChannel(ctx context.Context, channelID string) (int, error)
// ── Tree operations (v0.29.0 — moved from treepath package) ──
// GetActiveLeaf returns the cursor's active_leaf_id for a user in a channel.
// Falls back to the chronologically latest live message if no cursor exists.
GetActiveLeaf(ctx context.Context, channelID, userID string) (*string, error)
// GetPathToLeaf walks from a leaf up to root via parent_id, returning
// root-first order with sibling counts and sender info resolved.
GetPathToLeaf(ctx context.Context, channelID, leafID string) ([]PathMessage, error)
// GetActivePath is GetActiveLeaf + GetPathToLeaf combined.
GetActivePath(ctx context.Context, channelID, userID string) ([]PathMessage, error)
// GetSiblingsList returns all live siblings of a message with the current index.
GetSiblingsList(ctx context.Context, messageID string) ([]SiblingInfo, int, error)
// GetSiblingCount returns how many live siblings share the same parent.
GetSiblingCount(ctx context.Context, channelID string, parentID *string) (int, error)
// FindLeafFromMessage walks down from a message to find the deepest descendant.
FindLeafFromMessage(ctx context.Context, messageID string) (string, error)
// NextSiblingIndexForParent returns the next sibling_index for children of parentID.
NextSiblingIndexForParent(ctx context.Context, channelID string, parentID *string) (int, error)
// HasPersonaMessages checks if any assistant message in the channel was
// generated by a persona.
HasPersonaMessages(ctx context.Context, channelID string) (bool, error)
// CreateWithCursor inserts a message, updates the cursor, and touches
// channel.updated_at. Handles dialect-specific ID generation internally.
CreateWithCursor(ctx context.Context, m *models.Message, cursorUserID string) error
// ResolveSenderInfo batch-resolves sender_name and sender_avatar for
// user and persona participants. Updates the PathMessage slice in-place.
ResolveSenderInfo(ctx context.Context, path []PathMessage) error
// CountAll returns the total number of messages (for admin stats).
CountAll(ctx context.Context) (int, error)
// ── CS5c additions (v0.29.0) ──
// SearchInChannel performs full-text search within a single channel's messages.
// PG uses to_tsvector/ts_headline; SQLite falls back to LIKE.
// roleFilter is "user", "assistant", or "" for all.
SearchInChannel(ctx context.Context, channelID, query, roleFilter string, limit int) ([]ChannelSearchResult, error)
// ── CS7a additions (v0.29.0) ──
// ListWithSenderInfo returns paginated messages with sender name/avatar
// resolved via JOINs to users and personas tables.
ListWithSenderInfo(ctx context.Context, channelID string, limit, offset int) ([]MessageWithSender, int, error)
// GetParentAndRole loads just the parent_id and role of a message,
// verifying it belongs to the given channel and is not deleted.
GetParentAndRole(ctx context.Context, messageID, channelID string) (parentID *string, role string, err error)
}
// MessageWithSender is returned by MessageStore.ListWithSenderInfo.
type MessageWithSender struct {
ID string
ChannelID string
Role string
Content string
Model *string
TokensUsed *int
ParentID *string
SiblingIndex int
ParticipantType *string
ParticipantID *string
SenderName *string
SenderAvatar *string
CreatedAt string
}
// ChannelSearchResult is returned by MessageStore.SearchInChannel.
type ChannelSearchResult struct {
MessageID string
Role string
Excerpt string // ts_headline or content substring
Rank float64
Timestamp time.Time
}
// =========================================
// AUDIT STORE
// =========================================
type AuditStore interface {
Log(ctx context.Context, entry *models.AuditEntry) error
List(ctx context.Context, opts AuditListOptions) ([]models.AuditEntry, int, error)
ListActions(ctx context.Context) ([]string, error)
}
type AuditListOptions struct {
ListOptions
ActorID string
Action string
ResourceType string
ResourceID string
Since *time.Time
Until *time.Time
TeamID string // CS6: scope to team members
}
// =========================================
// NOTE STORE
// =========================================
type NoteStore interface {
Create(ctx context.Context, n *models.Note) error
GetByID(ctx context.Context, id string) (*models.Note, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error
ListForUser(ctx context.Context, userID string, opts NoteListOptions) ([]models.Note, int, error)
Search(ctx context.Context, userID, query string, opts ListOptions) ([]models.Note, int, error)
SearchTitles(ctx context.Context, userID, query string, limit int) ([]models.Note, error)
BulkDelete(ctx context.Context, ids []string, userID string) (int, error)
// ── CS5c additions (v0.29.0) ──
// SetEmbedding stores a pgvector embedding on a note. PG-only; SQLite no-ops.
SetEmbedding(ctx context.Context, noteID, vecStr string) error
// SearchKeyword performs full-text keyword search with ranking and headlines.
// PG uses ts_rank/ts_headline; SQLite falls back to LIKE.
SearchKeyword(ctx context.Context, userID, query string, limit int) ([]NoteSearchResult, error)
// SearchSemantic performs vector similarity search against note embeddings.
// PG-only; SQLite returns empty results.
SearchSemantic(ctx context.Context, userID, vecStr string, limit int) ([]NoteSearchResult, error)
// ListFolders returns distinct folder paths and note counts for a user.
ListFolders(ctx context.Context, userID string) ([]FolderInfo, error)
}
type NoteListOptions struct {
ListOptions
FolderPath string
Tag string
TeamID string
}
// NoteSearchResult is returned by NoteStore.SearchKeyword and SearchSemantic.
type NoteSearchResult struct {
ID string
Title string
FolderPath string
Tags []string
Excerpt string // truncated content preview
Headline string // ts_headline markup (empty on SQLite)
Rank float64 // ts_rank or similarity score
}
// FolderInfo is returned by NoteStore.ListFolders.
type FolderInfo struct {
Path string
Count int
}
// =========================================
// NOTE LINK STORE
// =========================================
// NoteLinkStore manages wikilink edges between notes.
type NoteLinkStore interface {
// ReplaceLinks deletes existing links for sourceNoteID and inserts new ones.
ReplaceLinks(ctx context.Context, sourceNoteID string, links []models.NoteLink) error
// ResolveByTitle sets target_note_id on dangling links matching the title for a user.
ResolveByTitle(ctx context.Context, userID, targetNoteID, title string) error
// Backlinks returns notes that link to the given note.
Backlinks(ctx context.Context, noteID string) ([]models.NoteLinkResult, error)
// Graph returns all nodes and edges for a user's note graph.
Graph(ctx context.Context, userID string) (*models.NoteGraph, error)
}
// =========================================
// GLOBAL CONFIG STORE
// =========================================
type GlobalConfigStore interface {
Get(ctx context.Context, key string) (models.JSONMap, error)
Set(ctx context.Context, key string, value models.JSONMap, updatedBy string) error
GetAll(ctx context.Context) (map[string]models.JSONMap, error)
// ── OIDC state (v0.29.0-cs4) ──
// SaveOIDCState stores a state+nonce pair for OIDC callback verification.
SaveOIDCState(ctx context.Context, state, nonce, redirectTo string) error
// ConsumeOIDCState retrieves and deletes an OIDC state (one-time use).
// Returns (nonce, redirectTo, error). Returns sql.ErrNoRows if not found.
ConsumeOIDCState(ctx context.Context, state string) (nonce, redirectTo string, err error)
// CleanupOIDCState removes stale OIDC states older than 10 minutes.
CleanupOIDCState(ctx context.Context) error
// ── CS6 additions (v0.29.0) ──
// GetString returns the raw value column as a string for simple settings
// (e.g. bare JSON booleans like "true"/"false").
GetString(ctx context.Context, key string) (string, error)
}
// =========================================
// USAGE STORE
// =========================================
type UsageStore interface {
Log(ctx context.Context, entry *models.UsageEntry) error
CountRecentByRole(ctx context.Context, userID, role string, since time.Time) (int, error)
QueryByUser(ctx context.Context, userID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
QueryByUserPersonal(ctx context.Context, userID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
QueryByTeam(ctx context.Context, teamID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
QueryByTeamProviders(ctx context.Context, teamID string, opts UsageQueryOptions) ([]models.UsageAggregate, error)
QueryByModel(ctx context.Context, opts UsageQueryOptions) ([]models.UsageAggregate, error)
GetTotals(ctx context.Context, opts UsageQueryOptions) (*models.UsageTotals, error)
GetTeamProviderTotals(ctx context.Context, teamID string, opts UsageQueryOptions) (*models.UsageTotals, error)
GetPersonalTotals(ctx context.Context, userID string, opts UsageQueryOptions) (*models.UsageTotals, error)
}
type UsageQueryOptions struct {
Since *time.Time
Until *time.Time
GroupBy string // "day", "model", "user", "provider"
ExcludeBYOK bool // true for admin views — filters provider_scope != 'personal'
Limit int
}
// =========================================
// PRICING STORE
// =========================================
type PricingStore interface {
GetForModel(ctx context.Context, providerConfigID, modelID string) (*models.PricingEntry, error)
Upsert(ctx context.Context, entry *models.PricingEntry) error
UpsertFromCatalog(ctx context.Context, providerConfigID, modelID string, pricing *models.ModelPricing) error
List(ctx context.Context) ([]models.PricingEntry, error)
Delete(ctx context.Context, providerConfigID, modelID string) error
}
// =========================================
// FILE STORE
// =========================================
type FileStore interface {
Create(ctx context.Context, f *models.File) error
GetByID(ctx context.Context, id string) (*models.File, error)
GetByChannel(ctx context.Context, channelID string, origin string) ([]models.File, error)
GetByMessage(ctx context.Context, messageID string) ([]models.File, error)
GetByProject(ctx context.Context, projectID string) ([]models.File, error)
GetByUser(ctx context.Context, userID string, page, perPage int) ([]models.File, int, error) // paginated, returns total
SetMessageID(ctx context.Context, fileID, messageID string) error
UpdateMetadata(ctx context.Context, id string, metadata map[string]interface{}) error
SetExtractedText(ctx context.Context, id string, text string) error
Delete(ctx context.Context, id string) (*models.File, error) // returns deleted row for storage cleanup
DeleteByChannel(ctx context.Context, channelID string) ([]string, error) // returns storage_keys
UserUsageBytes(ctx context.Context, userID string) (int64, error)
ListOrphans(ctx context.Context, olderThan time.Duration) ([]models.File, error)
// ── CS5c additions (v0.29.0) ──
// UpdateStorageKey sets the storage key after initial file creation.
UpdateStorageKey(ctx context.Context, id, key string) error
}
// =========================================
// KNOWLEDGE BASE STORE
// =========================================
type KnowledgeBaseStore interface {
// KB CRUD
Create(ctx context.Context, kb *models.KnowledgeBase) error
GetByID(ctx context.Context, id string) (*models.KnowledgeBase, error)
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id string) error
// Scoped listing
ListForUser(ctx context.Context, userID string, teamIDs []string) ([]models.KnowledgeBase, error)
ListPersonal(ctx context.Context, userID string) ([]models.KnowledgeBase, error)
// Documents
CreateDocument(ctx context.Context, doc *models.KBDocument) error
GetDocument(ctx context.Context, id string) (*models.KBDocument, error)
ListDocuments(ctx context.Context, kbID string) ([]models.KBDocument, error)
UpdateDocumentStatus(ctx context.Context, id string, status string, errMsg *string) error
UpdateDocumentText(ctx context.Context, id string, text string, chunkCount int) error
UpdateDocumentStorageKey(ctx context.Context, id string, storageKey string) error
DeleteDocument(ctx context.Context, id string) (*models.KBDocument, error) // returns for storage cleanup
// Chunks
InsertChunks(ctx context.Context, chunks []models.KBChunk) error
DeleteChunksForDocument(ctx context.Context, documentID string) error
SimilaritySearch(ctx context.Context, kbIDs []string, queryVec []float64,
threshold float64, limit int) ([]models.KBSearchResult, error)
// Channel links
SetChannelKBs(ctx context.Context, channelID string, kbIDs []string) error
GetChannelKBs(ctx context.Context, channelID string) ([]models.ChannelKB, error)
GetActiveKBIDs(ctx context.Context, channelID string, userID string,
teamIDs []string) ([]string, error) // enabled + user has access
GetActiveKBIDsWithPersona(ctx context.Context, channelID string, userID string,
teamIDs []string, personaID string) ([]string, error) // includes persona-bound KBs
ListDiscoverable(ctx context.Context, userID string, teamIDs []string) ([]models.KnowledgeBase, error)
// Stats — recount document_count/chunk_count/total_bytes from child tables
UpdateStats(ctx context.Context, kbID string) error
// Discoverable management
SetDiscoverable(ctx context.Context, kbID string, discoverable bool) error
}
// =========================================
// GROUP STORE (v0.16.0)
// =========================================
type GroupStore interface {
Create(ctx context.Context, g *models.Group) error
GetByID(ctx context.Context, id string) (*models.Group, error)
Update(ctx context.Context, id string, patch models.GroupPatch) error
Delete(ctx context.Context, id string) error
// Scoped listing
ListAll(ctx context.Context) ([]models.Group, error) // admin
ListForTeam(ctx context.Context, teamID string) ([]models.Group, error) // team admin
ListForUser(ctx context.Context, userID string) ([]models.Group, error) // groups I belong to
// Members
AddMember(ctx context.Context, groupID, userID, addedBy string) error
RemoveMember(ctx context.Context, groupID, userID string) error
ListMembers(ctx context.Context, groupID string) ([]models.GroupMember, error)
IsMember(ctx context.Context, groupID, userID string) (bool, error)
// For access resolution: returns group IDs a user belongs to
GetUserGroupIDs(ctx context.Context, userID string) ([]string, error)
}
// =========================================
// RESOURCE GRANT STORE (v0.16.0)
// =========================================
type ResourceGrantStore interface {
Set(ctx context.Context, grant *models.ResourceGrant) error
Get(ctx context.Context, resourceType, resourceID string) (*models.ResourceGrant, error)
Delete(ctx context.Context, resourceType, resourceID string) error
// Access check: does userID have group-based access to this resource?
UserHasGroupAccess(ctx context.Context, userID, resourceType, resourceID string) (bool, error)
}
// =========================================
// NOTIFICATION STORE (v0.20.0)
// =========================================
type NotificationStore interface {
Create(ctx context.Context, n *models.Notification) error
ListByUser(ctx context.Context, userID string, limit, offset int, unreadOnly bool) ([]models.Notification, int, error)
MarkRead(ctx context.Context, id, userID string) error
MarkAllRead(ctx context.Context, userID string) error
Delete(ctx context.Context, id, userID string) error
UnreadCount(ctx context.Context, userID string) (int, error)
DeleteOlderThan(ctx context.Context, before time.Time) (int64, error)
}
// =========================================
// NOTIFICATION PREFERENCES STORE (v0.20.0)
// =========================================
type NotificationPreferenceStore interface {
// Get returns the preference for a specific user + type. Returns nil if not set.
Get(ctx context.Context, userID, notifType string) (*models.NotificationPreference, error)
// ListForUser returns all preferences set by a user.
ListForUser(ctx context.Context, userID string) ([]models.NotificationPreference, error)
// Upsert creates or updates a preference row.
Upsert(ctx context.Context, pref *models.NotificationPreference) error
// Delete removes a preference (falls back to default).
Delete(ctx context.Context, userID, notifType string) error
}
// =========================================
// WORKSPACE STORE (v0.21.0)
// =========================================
type WorkspaceStore interface {
// CRUD
Create(ctx context.Context, w *models.Workspace) error
GetByID(ctx context.Context, id string) (*models.Workspace, error)
Update(ctx context.Context, id string, patch models.WorkspacePatch) error
Delete(ctx context.Context, id string) error
// Ownership lookup
GetByOwner(ctx context.Context, ownerType, ownerID string) (*models.Workspace, error)
ListByOwner(ctx context.Context, ownerType, ownerID string) ([]models.Workspace, error)
// File index
UpsertFile(ctx context.Context, f *models.WorkspaceFile) error
DeleteFile(ctx context.Context, workspaceID, path string) error
DeleteFilesByPrefix(ctx context.Context, workspaceID, prefix string) error
GetFile(ctx context.Context, workspaceID, path string) (*models.WorkspaceFile, error)
ListFiles(ctx context.Context, workspaceID, prefix string, recursive bool) ([]models.WorkspaceFile, error)
DeleteAllFiles(ctx context.Context, workspaceID string) error
// Stats
GetStats(ctx context.Context, workspaceID string) (*models.WorkspaceStats, error)
// Chunks (v0.21.2 — workspace indexing)
InsertChunks(ctx context.Context, chunks []models.WorkspaceChunk) error
DeleteChunksByFile(ctx context.Context, fileID string) error
SimilaritySearch(ctx context.Context, workspaceID string, queryVec []float64, threshold float64, limit int) ([]models.WorkspaceChunkResult, error)
UpdateFileIndexStatus(ctx context.Context, fileID, status string, chunkCount int) error
// Git (v0.21.4)
SetGitLastSync(ctx context.Context, workspaceID string, t time.Time) error
}
// =========================================
// GIT CREDENTIAL STORE (v0.21.4)
// =========================================
type GitCredentialStore interface {
Create(ctx context.Context, cred *models.GitCredential) error
GetByID(ctx context.Context, id string) (*models.GitCredential, error)
ListByUser(ctx context.Context, userID string) ([]models.GitCredential, error)
Delete(ctx context.Context, id, userID string) error
}
// =========================================
// CAPABILITY OVERRIDES (v0.22.0)
// =========================================
type CapabilityOverrideStore interface {
// Set creates or updates an override for (provider, model, field).
Set(ctx context.Context, o *models.CapabilityOverride) error
// Delete removes a specific override.
Delete(ctx context.Context, id string) error
// ListForModel returns all overrides for a model ID (across all providers + global).
ListForModel(ctx context.Context, modelID string) ([]models.CapabilityOverride, error)
// ListForProviderModel returns overrides for a specific provider+model combination.
ListForProviderModel(ctx context.Context, providerConfigID, modelID string) ([]models.CapabilityOverride, error)
// ListAll returns every override (admin view).
ListAll(ctx context.Context) ([]models.CapabilityOverride, error)
// DeleteForProvider removes all overrides for a provider (cascade cleanup).
DeleteForProvider(ctx context.Context, providerConfigID string) error
}
// =========================================
// ROUTING POLICY STORE (v0.22.2)
// =========================================
type RoutingPolicyStore interface {
// Create adds a new routing policy.
Create(ctx context.Context, p *models.RoutingPolicy) error
// Update modifies an existing routing policy.
Update(ctx context.Context, p *models.RoutingPolicy) error
// Delete removes a routing policy by ID.
Delete(ctx context.Context, id string) error
// GetByID returns a single policy.
GetByID(ctx context.Context, id string) (*models.RoutingPolicy, error)
// ListActive returns all active policies, ordered by priority ASC.
ListActive(ctx context.Context) ([]models.RoutingPolicy, error)
// ListAll returns all policies (including inactive), for admin listing.
ListAll(ctx context.Context) ([]models.RoutingPolicy, error)
// ListForTeam returns active policies applicable to a team (team-scoped + global).
ListForTeam(ctx context.Context, teamID string) ([]models.RoutingPolicy, error)
}
// =========================================
// SESSION STORE (v0.24.3)
// =========================================
type SessionStore interface {
Create(ctx context.Context, s *models.SessionParticipant) error
GetByToken(ctx context.Context, token string) (*models.SessionParticipant, error)
GetByID(ctx context.Context, id string) (*models.SessionParticipant, error)
ListForChannel(ctx context.Context, channelID string) ([]models.SessionParticipant, error)
CountForChannel(ctx context.Context, channelID string) (int, error)
Delete(ctx context.Context, id string) error
// DeleteExpired removes sessions older than the given time that have
// no associated messages. Returns the number of deleted rows.
// Used by the background session cleanup job (v0.26.0).
DeleteExpired(ctx context.Context, olderThan time.Time) (int64, error)
}
// =========================================
// PERSONA GROUP STORE (v0.29.0)
// =========================================
// PersonaGroupStore manages persona_groups and their members.
type PersonaGroupStore interface {
// CRUD
List(ctx context.Context, ownerID string) ([]models.PersonaGroup, error)
Get(ctx context.Context, id, ownerID string) (*models.PersonaGroup, error)
Create(ctx context.Context, g *models.PersonaGroup) error
Update(ctx context.Context, id string, fields map[string]interface{}) error
Delete(ctx context.Context, id, ownerID string) (int64, error)
// Ownership check
GetOwnerID(ctx context.Context, id string) (string, error)
// Members
AddMember(ctx context.Context, groupID, personaID string, isLeader bool) error
RemoveMember(ctx context.Context, memberID, groupID string) error
ListMembers(ctx context.Context, groupID string) ([]models.PersonaGroupMember, error)
}
// =========================================
// FOLDER STORE (v0.29.0)
// =========================================
// FolderStore manages user chat folders.
type FolderStore interface {
List(ctx context.Context, userID string) ([]models.Folder, error)
Create(ctx context.Context, f *models.Folder) error
Update(ctx context.Context, folderID, userID string, name string, sortOrder *int, parentID **string) (int64, error)
Delete(ctx context.Context, folderID, userID string) (int64, error)
// UnassignChannels removes folder_id from all channels in this folder.
UnassignChannels(ctx context.Context, folderID, userID string) error
}
// =========================================
// HEALTH STORE (v0.29.0)
// =========================================
// HealthStore manages provider health windows. The full interface is used
// by the health accumulator and status querier; the scheduler only needs Prune.
type HealthStore interface {
// Prune deletes health windows older than the given time. Returns rows affected.
Prune(ctx context.Context, before time.Time) (int64, error)
}
// =========================================
// PRESENCE STORE (v0.29.0)
// =========================================
// PresenceStore manages user online/offline status.
type PresenceStore interface {
// Heartbeat upserts the user's last_seen timestamp to now.
Heartbeat(ctx context.Context, userID string) error
// GetLastSeen returns the user's last heartbeat time, or nil if never seen.
GetLastSeen(ctx context.Context, userID string) (*time.Time, error)
// GetStatuses returns online/offline status for a list of user IDs.
// Users with last_seen after threshold are "online", otherwise "offline".
GetStatuses(ctx context.Context, userIDs []string, threshold time.Time) (map[string]string, error)
}
// =========================================
// EXPORT STORE (v0.34.0)
// =========================================
// ExportStore provides batch-read queries for data export, import,
// and GDPR operations. These methods are optimized for bulk retrieval
// without N+1 queries. Embeddings are excluded at query level.
type ExportStore interface {
// ── User-scoped export ──
// UserChannels returns all channels owned by userID.
UserChannels(ctx context.Context, userID string) ([]models.Channel, error)
// UserMessages returns all non-deleted messages in the given channels, ordered by created_at ASC.
UserMessages(ctx context.Context, channelIDs []string) ([]models.Message, error)
// UserChannelParticipants returns participants for the given channels.
UserChannelParticipants(ctx context.Context, channelIDs []string) ([]models.ChannelParticipant, error)
// UserChannelModels returns channel_models for the given channels.
UserChannelModels(ctx context.Context, channelIDs []string) ([]models.ChannelModel, error)
// UserChannelCursors returns cursors for the user in the given channels.
UserChannelCursors(ctx context.Context, userID string, channelIDs []string) ([]models.ChannelCursor, error)
// UserNotes returns all notes owned by userID.
UserNotes(ctx context.Context, userID string) ([]models.Note, error)
// UserNoteLinks returns note_links with source_note_id for the given note IDs.
UserNoteLinks(ctx context.Context, noteIDs []string) ([]models.ExportNoteLink, error)
// UserMemories returns active memories for userID (scope=user).
UserMemories(ctx context.Context, userID string) ([]models.Memory, error)
// UserProjects returns personal-scope projects owned by userID.
UserProjects(ctx context.Context, userID string) ([]models.Project, error)
// UserProjectChannels returns project_channels for the given projects.
UserProjectChannels(ctx context.Context, projectIDs []string) ([]models.ProjectChannel, error)
// UserProjectKBs returns project_knowledge_bases for the given projects.
UserProjectKBs(ctx context.Context, projectIDs []string) ([]models.ProjectKB, error)
// UserProjectNotes returns project_notes for the given projects.
UserProjectNotes(ctx context.Context, projectIDs []string) ([]models.ProjectNote, error)
// UserWorkspaces returns workspaces owned by user (owner_type=user).
UserWorkspaces(ctx context.Context, userID string) ([]models.Workspace, error)
// UserWorkspaceFiles returns file index for the given workspaces.
UserWorkspaceFiles(ctx context.Context, workspaceIDs []string) ([]models.WorkspaceFile, error)
// UserFiles returns file metadata for files uploaded by userID.
UserFiles(ctx context.Context, userID string) ([]models.File, error)
// UserFolders returns all folders for userID.
UserFolders(ctx context.Context, userID string) ([]models.Folder, error)
// UserModelSettings returns user_model_settings for userID.
UserModelSettings(ctx context.Context, userID string) ([]models.UserModelSetting, error)
// UserNotifPrefs returns notification preferences for userID.
UserNotifPrefs(ctx context.Context, userID string) ([]models.NotificationPreference, error)
// UserUsageEntries returns usage entries for userID (last 90 days).
UserUsageEntries(ctx context.Context, userID string) ([]models.UsageEntry, error)
// UserPersonaGroups returns persona groups owned by userID.
UserPersonaGroups(ctx context.Context, userID string) ([]models.PersonaGroup, error)
// UserPersonaGroupMembers returns members for the given groups.
UserPersonaGroupMembers(ctx context.Context, groupIDs []string) ([]models.PersonaGroupMember, error)
// ── Team-scoped export ──
// TeamChannels returns all channels belonging to teamID.
TeamChannels(ctx context.Context, teamID string) ([]models.Channel, error)
// TeamMembers returns team membership records.
TeamMembers(ctx context.Context, teamID string) ([]models.TeamMember, error)
// TeamPersonas returns team-scoped personas.
TeamPersonas(ctx context.Context, teamID string) ([]models.Persona, error)
// TeamPersonaKBs returns persona_knowledge_bases for given persona IDs.
TeamPersonaKBs(ctx context.Context, personaIDs []string) ([]models.PersonaKB, error)
// TeamKnowledgeBases returns team-scoped knowledge bases.
TeamKnowledgeBases(ctx context.Context, teamID string) ([]models.KnowledgeBase, error)
// TeamKBDocuments returns KB document metadata for given KB IDs (no chunks/embeddings).
TeamKBDocuments(ctx context.Context, kbIDs []string) ([]models.KBDocument, error)
// TeamWorkflows returns team-scoped workflows.
TeamWorkflows(ctx context.Context, teamID string) ([]models.Workflow, error)
// TeamWorkflowVersions returns versions for given workflow IDs.
TeamWorkflowVersions(ctx context.Context, workflowIDs []string) ([]models.WorkflowVersion, error)
// TeamWorkflowStages returns stages for given workflow IDs.
TeamWorkflowStages(ctx context.Context, workflowIDs []string) ([]models.WorkflowStage, error)
// TeamGroups returns team-scoped groups.
TeamGroups(ctx context.Context, teamID string) ([]models.Group, error)
// TeamGroupMembers returns members for given group IDs.
TeamGroupMembers(ctx context.Context, groupIDs []string) ([]models.GroupMember, error)
// TeamResourceGrants returns resource grants for team resources.
TeamResourceGrants(ctx context.Context, teamID string) ([]models.ResourceGrant, error)
// TeamProjects returns team-scoped projects.
TeamProjects(ctx context.Context, teamID string) ([]models.Project, error)
// ── Import (CS1) ──
// Each ImportXxx method inserts entities preserving their original IDs.
// Uses INSERT ON CONFLICT DO NOTHING (PG) / INSERT OR IGNORE (SQLite)
// to skip duplicates. Returns (imported, skipped) counts.
ImportFolders(ctx context.Context, folders []models.Folder) (int, int, error)
ImportProjects(ctx context.Context, projects []models.Project) (int, int, error)
ImportWorkspaces(ctx context.Context, workspaces []models.Workspace) (int, int, error)
ImportChannels(ctx context.Context, channels []models.Channel) (int, int, error)
ImportChannelParticipants(ctx context.Context, cps []models.ChannelParticipant) (int, int, error)
ImportChannelModels(ctx context.Context, cms []models.ChannelModel) (int, int, error)
ImportChannelCursors(ctx context.Context, cursors []models.ChannelCursor) (int, int, error)
ImportMessages(ctx context.Context, msgs []models.Message) (int, int, error)
ImportNotes(ctx context.Context, notes []models.Note) (int, int, error)
ImportNoteLinks(ctx context.Context, links []models.ExportNoteLink) (int, int, error)
ImportMemories(ctx context.Context, mems []models.Memory) (int, int, error)
ImportProjectChannels(ctx context.Context, pcs []models.ProjectChannel) (int, int, error)
ImportProjectKBs(ctx context.Context, pks []models.ProjectKB) (int, int, error)
ImportProjectNotes(ctx context.Context, pns []models.ProjectNote) (int, int, error)
ImportWorkspaceFiles(ctx context.Context, wfs []models.WorkspaceFile) (int, int, error)
ImportFiles(ctx context.Context, files []models.File) (int, int, error)
ImportUserModelSettings(ctx context.Context, ums []models.UserModelSetting) (int, int, error)
ImportNotifPrefs(ctx context.Context, nps []models.NotificationPreference) (int, int, error)
ImportPersonaGroups(ctx context.Context, pgs []models.PersonaGroup) (int, int, error)
ImportPersonaGroupMembers(ctx context.Context, pgms []models.PersonaGroupMember) (int, int, error)
// ── GDPR ──
// CountActiveAdmins returns the number of active admin users.
CountActiveAdmins(ctx context.Context) (int, error)
// AnonymizeUser replaces PII with "deleted-user-{hash}" placeholders.
AnonymizeUser(ctx context.Context, userID string, anonHash string) error
// SoftDeleteUserData cascade-deletes/soft-deletes all user-owned data.
// Returns entity counts by type for the audit trail.
SoftDeleteUserData(ctx context.Context, userID string) (map[string]int64, error)
// DeleteUserTokens revokes all refresh tokens and WS tickets.
DeleteUserTokens(ctx context.Context, userID string) error
}
// =========================================
// SHARED TYPES
// =========================================
// CONNECTION STORE (v0.38.1)
// =========================================
type ConnectionStore interface {
Create(ctx context.Context, conn *models.ExtConnection) error
GetByID(ctx context.Context, id string) (*models.ExtConnection, error)
Update(ctx context.Context, id string, patch models.ExtConnectionPatch) error
Delete(ctx context.Context, id string) error
// Scoped queries
ListGlobal(ctx context.Context) ([]models.ExtConnection, error)
ListForTeam(ctx context.Context, teamID string) ([]models.ExtConnection, error)
ListForUser(ctx context.Context, userID string) ([]models.ExtConnection, error)
// Resolution chain: personal → team → global.
// Returns first active connection matching type (and optional name).
// Empty name means "first match". Returns sql.ErrNoRows if not found.
Resolve(ctx context.Context, userID, connType, name string) (*models.ExtConnection, error)
// ResolveAll returns all active connections of the given type accessible
// to the user (personal + team + global).
ResolveAll(ctx context.Context, userID, connType string) ([]models.ExtConnection, error)
// ListAccessible returns all connections accessible to the user across
// all types: personal + team memberships + global.
ListAccessible(ctx context.Context, userID string) ([]models.ExtConnection, error)
// DeleteByIDAndScope deletes a connection only if it matches the given
// scope and owner. Returns rows affected (0 or 1).
DeleteByIDAndScope(ctx context.Context, id, scope, ownerID string) (int64, error)
}
// =========================================
// DEPENDENCY STORE (v0.38.2)
// =========================================
type DependencyStore interface {
// Create records that consumerID depends on libraryID.
Create(ctx context.Context, dep *models.ExtDependency) error
// Delete removes a single dependency record.
Delete(ctx context.Context, consumerID, libraryID string) error
// DeleteAllForConsumer removes all dependency records for a consumer.
// Called when uninstalling a consumer package.
DeleteAllForConsumer(ctx context.Context, consumerID string) error
// ListByConsumer returns all libraries that consumerID depends on.
ListByConsumer(ctx context.Context, consumerID string) ([]models.ExtDependency, error)
// ListByLibrary returns all consumers that depend on libraryID.
ListByLibrary(ctx context.Context, libraryID string) ([]models.ExtDependency, error)
// ListAll returns the full dependency graph.
ListAll(ctx context.Context) ([]models.ExtDependency, error)
// HasConsumers returns true if any package depends on libraryID.
// Used for uninstall protection.
HasConsumers(ctx context.Context, libraryID string) (bool, error)
}
// =========================================
// ListOptions provides standard pagination/sort for list queries.
type ListOptions struct {
Limit int
Offset int
Sort string // column name
Order string // "asc" or "desc"
}
// DefaultListOptions returns sensible defaults.
func DefaultListOptions() ListOptions {
return ListOptions{
Limit: 50,
Order: "desc",
}
}