1212 lines
52 KiB
Go
1212 lines
52 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/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
|
|
}
|
|
|
|
// =========================================
|
|
// 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)
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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) (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)
|
|
}
|
|
|
|
// =========================================
|
|
// 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",
|
|
}
|
|
}
|