package store import ( "context" "time" "git.gobha.me/xcaliber/chat-switchboard/models" ) // ========================================= // 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 Extensions ExtensionStore Attachments AttachmentStore KnowledgeBases KnowledgeBaseStore Groups GroupStore ResourceGrants ResourceGrantStore Memories MemoryStore Projects ProjectStore Notifications NotificationStore NotifPrefs NotificationPreferenceStore Workspaces WorkspaceStore } // ========================================= // 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) } // ========================================= // 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 } // 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) ListPersonal(ctx context.Context, userID string) ([]models.Persona, error) // user's own // 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) } // ========================================= // 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, patch models.UserModelSettingPatch) error BulkSetHidden(ctx context.Context, userID string, modelIDs []string, 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 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 // 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 } // ========================================= // 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) // 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) } // ========================================= // 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) // Workspace resolution: channel workspace_id > project workspace_id ResolveWorkspaceID(ctx context.Context, channelID string) (string, error) } // ========================================= // 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) } // ========================================= // AUDIT STORE // ========================================= type AuditStore interface { Log(ctx context.Context, entry *models.AuditEntry) error List(ctx context.Context, opts AuditListOptions) ([]models.AuditEntry, int, error) } type AuditListOptions struct { ListOptions ActorID string Action string ResourceType string ResourceID string Since *time.Time Until *time.Time } // ========================================= // 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) } type NoteListOptions struct { ListOptions FolderPath string Tag string TeamID string } // ========================================= // 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) } // ========================================= // 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 } // ========================================= // EXTENSION STORE // ========================================= type ExtensionStore interface { // Admin CRUD Create(ctx context.Context, ext *models.Extension) error GetByID(ctx context.Context, id string) (*models.Extension, error) GetByExtID(ctx context.Context, extID string) (*models.Extension, error) Update(ctx context.Context, id string, ext *models.Extension) error Delete(ctx context.Context, id string) error // Listing ListAll(ctx context.Context) ([]models.Extension, error) ListEnabled(ctx context.Context) ([]models.Extension, error) ListForUser(ctx context.Context, userID string) ([]models.UserExtension, error) // User settings GetUserSettings(ctx context.Context, extID, userID string) (*models.ExtensionUserSettings, error) SetUserSettings(ctx context.Context, s *models.ExtensionUserSettings) error DeleteUserSettings(ctx context.Context, extID, userID string) error } // ========================================= // ATTACHMENT STORE // ========================================= type AttachmentStore interface { Create(ctx context.Context, a *models.Attachment) error GetByID(ctx context.Context, id string) (*models.Attachment, error) GetByChannel(ctx context.Context, channelID string) ([]models.Attachment, error) GetByMessage(ctx context.Context, messageID string) ([]models.Attachment, error) SetMessageID(ctx context.Context, attachmentID, 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.Attachment, 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.Attachment, 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) ListGlobal(ctx context.Context) ([]models.KnowledgeBase, error) ListForTeam(ctx context.Context, teamID 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, name, description *string) 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) } // ========================================= // SHARED TYPES // ========================================= // 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", } }