Changeset 0.32.0 (#206)
This commit is contained in:
@@ -6,8 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type AuditStore struct{}
|
||||
|
||||
@@ -3,8 +3,8 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type CapOverrideStore struct{}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type CatalogStore struct{}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type ChannelStore struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/database"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/database"
|
||||
"chat-switchboard/models"
|
||||
)
|
||||
|
||||
type ExtensionPermissionStore struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// FileStore implements store.FileStore against the `files` table.
|
||||
|
||||
@@ -3,8 +3,8 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type FolderStore struct{}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/models"
|
||||
)
|
||||
|
||||
type GlobalConfigStore struct{}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── GroupStore ──────────────────────────────
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type HealthStore struct{}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// DB is the shared database connection pool.
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── KnowledgeBaseStore ──────────────────────────
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type MemoryStore struct{}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/models"
|
||||
)
|
||||
|
||||
// RecallHybrid returns active memories using vector similarity + keyword search.
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type MessageStore struct{}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── Tree Operations (v0.29.0) ───────────────────────────────────────────
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type NoteStore struct{}
|
||||
|
||||
@@ -3,7 +3,7 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/models"
|
||||
)
|
||||
|
||||
type NoteLinkStore struct{}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type NotificationStore struct{}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type NotificationPreferenceStore struct{}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type PackageStore struct{}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type PersonaStore struct{}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type PersonaGroupStore struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── Mention resolution + display info (v0.29.0) ────────────────────────
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"chat-switchboard/models"
|
||||
)
|
||||
|
||||
type PolicyStore struct{}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// PresenceStore manages user_presence table.
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type PricingStore struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── ProjectStore ───────────────────────────
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type ProviderStore struct{}
|
||||
|
||||
46
server/store/sqlite/ratelimit.go
Normal file
46
server/store/sqlite/ratelimit.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimitStore manages rate limit counters in SQLite.
|
||||
// v0.32.0: functional parity for single-process test coverage.
|
||||
type RateLimitStore struct{}
|
||||
|
||||
func NewRateLimitStore() *RateLimitStore { return &RateLimitStore{} }
|
||||
|
||||
func (s *RateLimitStore) Allow(ctx context.Context, key string, rate float64, burst int) (bool, error) {
|
||||
// SQLite: strftime truncates to the second for window bucketing.
|
||||
window := time.Now().UTC().Truncate(time.Second).Format("2006-01-02 15:04:05")
|
||||
|
||||
// Upsert via INSERT OR REPLACE pattern. SQLite has no ON CONFLICT ... DO UPDATE
|
||||
// with RETURNING, so read-then-write in a single connection (single-writer safe).
|
||||
var tokens float64
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(tokens, 0) FROM rate_limit_counters
|
||||
WHERE key = ? AND window_start = ?`, key, window).Scan(&tokens)
|
||||
if err != nil {
|
||||
// No row yet — insert fresh
|
||||
tokens = 0
|
||||
}
|
||||
tokens++
|
||||
_, err = DB.ExecContext(ctx, `
|
||||
INSERT INTO rate_limit_counters (key, window_start, tokens)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (key, window_start) DO UPDATE SET tokens = ?`,
|
||||
key, window, tokens, tokens)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("rate limit upsert: %w", err)
|
||||
}
|
||||
return tokens <= float64(burst), nil
|
||||
}
|
||||
|
||||
func (s *RateLimitStore) Cleanup(ctx context.Context, maxAge time.Duration) error {
|
||||
cutoff := time.Now().UTC().Add(-maxAge).Format("2006-01-02 15:04:05")
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM rate_limit_counters WHERE window_start < ?`, cutoff)
|
||||
return err
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── ResourceGrantStore ──────────────────────
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// RoutingPolicyStore implements store.RoutingPolicyStore for SQLite.
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── SessionStore ───────────────────────────
|
||||
|
||||
@@ -3,7 +3,7 @@ package sqlite
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// NewStores creates all SQLite store implementations and wires them
|
||||
@@ -48,5 +48,7 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
Health: NewHealthStore(),
|
||||
ExtPermissions: NewExtensionPermissionStore(),
|
||||
ExtData: NewExtDataStore(),
|
||||
Tickets: NewTicketStore(),
|
||||
RateLimits: NewRateLimitStore(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type TaskStore struct{}
|
||||
@@ -159,8 +160,20 @@ func (s *TaskStore) ListAll(ctx context.Context) ([]models.Task, error) {
|
||||
return s.list(ctx, `ORDER BY created_at DESC`)
|
||||
}
|
||||
|
||||
func (s *TaskStore) ListDue(ctx context.Context, limit int) ([]models.Task, error) {
|
||||
return s.list(ctx, `WHERE is_active = 1 AND next_run_at <= datetime('now') ORDER BY next_run_at ASC LIMIT ?`, limit)
|
||||
func (s *TaskStore) ClaimDueTask(ctx context.Context) (*models.Task, error) {
|
||||
// SQLite: single-process, no contention. Select the oldest due task,
|
||||
// then atomically clear next_run_at to mark it claimed.
|
||||
t := &models.Task{}
|
||||
row := DB.QueryRowContext(ctx,
|
||||
`SELECT `+taskColumns+` FROM tasks
|
||||
WHERE is_active = 1 AND next_run_at <= datetime('now')
|
||||
ORDER BY next_run_at ASC LIMIT 1`)
|
||||
if err := scanTask(row, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, _ = DB.ExecContext(ctx,
|
||||
`UPDATE tasks SET next_run_at = NULL WHERE id = ?`, t.ID)
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ── Scheduler bookkeeping ──────────────────────
|
||||
@@ -194,6 +207,34 @@ func (s *TaskStore) CreateRun(ctx context.Context, r *models.TaskRun) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TaskStore) CreateRunExclusive(ctx context.Context, taskID string) (*models.TaskRun, error) {
|
||||
// SQLite: check-then-insert. Single-process, no race.
|
||||
var count int
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM task_runs
|
||||
WHERE task_id = ? AND status IN ('running', 'queued')`, taskID).Scan(&count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
r := &models.TaskRun{
|
||||
ID: store.NewID(),
|
||||
TaskID: taskID,
|
||||
Status: "running",
|
||||
StartedAt: time.Now().UTC(),
|
||||
}
|
||||
_, err = DB.ExecContext(ctx, `
|
||||
INSERT INTO task_runs (id, task_id, status, started_at)
|
||||
VALUES (?, ?, 'running', ?)`,
|
||||
r.ID, taskID, r.StartedAt.UTC().Format("2006-01-02 15:04:05"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *TaskStore) UpdateRun(ctx context.Context, id, status string, tokensUsed, toolCalls, wallClock int, errMsg string) error {
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
UPDATE task_runs SET status = ?, tokens_used = ?, tool_calls = ?,
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type TeamStore struct{}
|
||||
@@ -22,9 +22,9 @@ func (s *TeamStore) Create(ctx context.Context, t *models.Team) error {
|
||||
t.CreatedAt = now
|
||||
t.UpdatedAt = now
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO teams (id, name, slug, description, created_by, is_active, settings, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ID, t.Name, t.Slug, t.Description, t.CreatedBy, t.IsActive, settingsJSON,
|
||||
INSERT INTO teams (id, name, description, created_by, is_active, settings, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ID, t.Name, t.Description, t.CreatedBy, t.IsActive, settingsJSON,
|
||||
now.Format(timeFmt), now.Format(timeFmt),
|
||||
)
|
||||
return err
|
||||
@@ -34,27 +34,10 @@ func (s *TeamStore) GetByID(ctx context.Context, id string) (*models.Team, error
|
||||
var t models.Team
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, name, slug, description, created_by, is_active, settings, created_at, updated_at,
|
||||
SELECT id, name, description, created_by, is_active, settings, created_at, updated_at,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count
|
||||
FROM teams WHERE id = ?`, id).Scan(
|
||||
&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MemberCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
json.Unmarshal(settingsJSON, &t.Settings)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *TeamStore) GetBySlug(ctx context.Context, slug string) (*models.Team, error) {
|
||||
var t models.Team
|
||||
var settingsJSON []byte
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, name, slug, description, created_by, is_active, settings, created_at, updated_at,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count
|
||||
FROM teams WHERE slug = ?`, slug).Scan(
|
||||
&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MemberCount,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -88,7 +71,7 @@ func (s *TeamStore) Delete(ctx context.Context, id string) error {
|
||||
|
||||
func (s *TeamStore) List(ctx context.Context) ([]models.Team, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, name, slug, description, created_by, is_active, settings, created_at, updated_at,
|
||||
SELECT id, name, description, created_by, is_active, settings, created_at, updated_at,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count
|
||||
FROM teams ORDER BY name`)
|
||||
if err != nil {
|
||||
@@ -100,7 +83,7 @@ func (s *TeamStore) List(ctx context.Context) ([]models.Team, error) {
|
||||
for rows.Next() {
|
||||
var t models.Team
|
||||
var settingsJSON []byte
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MemberCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -113,7 +96,7 @@ func (s *TeamStore) List(ctx context.Context) ([]models.Team, error) {
|
||||
|
||||
func (s *TeamStore) ListForUser(ctx context.Context, userID string) ([]models.Team, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT t.id, t.name, t.slug, t.description, t.created_by, t.is_active,
|
||||
SELECT t.id, t.name, t.description, t.created_by, t.is_active,
|
||||
COALESCE(t.settings, '{}'), t.created_at, t.updated_at,
|
||||
tm.role AS my_role,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = t.id) AS member_count
|
||||
@@ -130,7 +113,7 @@ func (s *TeamStore) ListForUser(ctx context.Context, userID string) ([]models.Te
|
||||
for rows.Next() {
|
||||
var t models.Team
|
||||
var settingsJSON []byte
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MyRole, &t.MemberCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
54
server/store/sqlite/tickets.go
Normal file
54
server/store/sqlite/tickets.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// TicketStore manages WS auth tickets in SQLite.
|
||||
// v0.32.0: functional parity for single-process test coverage.
|
||||
type TicketStore struct{}
|
||||
|
||||
func NewTicketStore() *TicketStore { return &TicketStore{} }
|
||||
|
||||
func (s *TicketStore) Issue(ctx context.Context, userID string) (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := hex.EncodeToString(b)
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO ws_tickets (id, user_id, expires_at)
|
||||
VALUES (?, ?, datetime('now', '+30 seconds'))`, id, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *TicketStore) Validate(ctx context.Context, ticketID string) (string, bool) {
|
||||
// SQLite has no DELETE ... RETURNING. Two-step: select then delete.
|
||||
var userID string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT user_id FROM ws_tickets
|
||||
WHERE id = ? AND expires_at > datetime('now')`, ticketID).Scan(&userID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", false
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
DB.ExecContext(ctx, `DELETE FROM ws_tickets WHERE id = ?`, ticketID)
|
||||
return userID, true
|
||||
}
|
||||
|
||||
func (s *TicketStore) Reap(ctx context.Context) (int, error) {
|
||||
res, err := DB.ExecContext(ctx, `DELETE FROM ws_tickets WHERE expires_at <= datetime('now')`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, _ := res.RowsAffected()
|
||||
return int(n), nil
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type UsageStore struct{}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type UserStore struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// ── Mention resolution + display info (v0.29.0) ────────────────────────
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type UserModelSettingsStore struct{}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
// WorkflowStore implements store.WorkflowStore for SQLite.
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
"chat-switchboard/models"
|
||||
"chat-switchboard/store"
|
||||
)
|
||||
|
||||
type WorkspaceStore struct{}
|
||||
|
||||
Reference in New Issue
Block a user