package store import ( "context" "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. // PolicyStore provides platform policy flags (allow_registration, etc.) type PolicyStore interface { GetBool(ctx context.Context, key string) (bool, error) SetBool(ctx context.Context, key string, val bool) error Get(ctx context.Context, key string) (string, error) Set(ctx context.Context, key, value string) error GetAll(ctx context.Context) (map[string]string, error) } type Stores struct { Users UserStore Teams TeamStore Audit AuditStore GlobalConfig GlobalConfigStore Policies PolicyStore Groups GroupStore ResourceGrants ResourceGrantStore Notifications NotificationStore NotifPrefs NotificationPreferenceStore Presence PresenceStore // v0.29.0: User online/offline status Health HealthStore // v0.29.0: Provider health window management Connections ConnectionStore // v0.38.1: Extension connection credentials Dependencies DependencyStore // v0.38.2: Library package dependency graph Packages PackageStore // v0.28.7: Unified package registry (surfaces + extensions) Workflows WorkflowStore // v0.26.1: Workflow definitions + stages 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 Triggers TriggerStore // v0.2.2: Extension event/webhook triggers ScheduledTasks ScheduledTaskStore // v0.2.2: User-created cron tasks Cluster ClusterStore // v0.6.0: PG-backed cluster node registry (nil on SQLite) } // TeamAvailableModel is returned by CatalogStore.ListTeamAvailable. // CatalogSyncEntry is the input format from provider FetchModels. // ========================================= // 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 // ── 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) ── // 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) // MergeSettings merges a JSON string into the team's settings column. MergeSettings(ctx context.Context, teamID, settingsJSON string) error } // ChannelListFilter holds filter options for ChannelStore.ListFiltered. // ChannelListItem is returned by ListFiltered and GetForUser. // ArchivedChannel is a view model for the admin archived channels list. // MessageWithSender is returned by MessageStore.ListWithSenderInfo. // ChannelSearchResult is returned by MessageStore.SearchInChannel. // ========================================= // 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 } // NoteSearchResult is returned by NoteStore.SearchKeyword and SearchSemantic. // FolderInfo is returned by NoteStore.ListFolders. // ========================================= // 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) } // ========================================= // 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 } // ========================================= // SESSION STORE (v0.24.3) // ========================================= // ========================================= // 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 // ========================================= // 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", } }