This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/store/sqlite/team.go
2026-03-19 13:29:27 +00:00

350 lines
10 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
"git.gobha.me/xcaliber/chat-switchboard/models"
"git.gobha.me/xcaliber/chat-switchboard/store"
)
type TeamStore struct{}
func NewTeamStore() *TeamStore { return &TeamStore{} }
func (s *TeamStore) Create(ctx context.Context, t *models.Team) error {
settingsJSON := ToJSON(t.Settings)
t.ID = store.NewID()
now := time.Now().UTC()
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,
now.Format(timeFmt), now.Format(timeFmt),
)
return err
}
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 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,
&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) Update(ctx context.Context, id string, fields map[string]interface{}) error {
b := NewUpdate("teams")
for k, v := range fields {
if k == "settings" {
b.SetJSON(k, v)
} else {
b.Set(k, v)
}
}
if !b.HasSets() {
return nil
}
b.Where("id", id)
_, err := b.Exec(DB)
return err
}
func (s *TeamStore) Delete(ctx context.Context, id string) error {
_, err := DB.ExecContext(ctx, "DELETE FROM teams WHERE id = ?", id)
return err
}
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 COUNT(*) FROM team_members WHERE team_id = teams.id) as member_count
FROM teams ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.Team
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,
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MemberCount)
if err != nil {
return nil, err
}
json.Unmarshal(settingsJSON, &t.Settings)
result = append(result, t)
}
return result, rows.Err()
}
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,
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
FROM teams t
JOIN team_members tm ON tm.team_id = t.id AND tm.user_id = ?
WHERE t.is_active = 1
ORDER BY t.name ASC`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.Team
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,
&settingsJSON, st(&t.CreatedAt), st(&t.UpdatedAt), &t.MyRole, &t.MemberCount)
if err != nil {
return nil, err
}
json.Unmarshal(settingsJSON, &t.Settings)
result = append(result, t)
}
return result, rows.Err()
}
// ── Members ─────────────────────────────────
func (s *TeamStore) AddMember(ctx context.Context, teamID, userID, role string) error {
_, err := DB.ExecContext(ctx, `
INSERT INTO team_members (id, team_id, user_id, role) VALUES (?, ?, ?, ?)
ON CONFLICT (team_id, user_id) DO UPDATE SET role = excluded.role`,
store.NewID(), teamID, userID, role)
return err
}
func (s *TeamStore) RemoveMember(ctx context.Context, teamID, userID string) error {
_, err := DB.ExecContext(ctx, "DELETE FROM team_members WHERE team_id = ? AND user_id = ?",
teamID, userID)
return err
}
func (s *TeamStore) UpdateMemberRole(ctx context.Context, teamID, userID, role string) error {
_, err := DB.ExecContext(ctx,
"UPDATE team_members SET role = ? WHERE team_id = ? AND user_id = ?",
role, teamID, userID)
return err
}
func (s *TeamStore) ListMembers(ctx context.Context, teamID string) ([]models.TeamMember, error) {
rows, err := DB.QueryContext(ctx, `
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at,
u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role
FROM team_members tm
JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ?
ORDER BY tm.role DESC, u.username`, teamID)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.TeamMember
for rows.Next() {
var m models.TeamMember
err := rows.Scan(&m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt,
&m.Email, &m.DisplayName, &m.Username, &m.UserRole)
if err != nil {
return nil, err
}
result = append(result, m)
}
return result, rows.Err()
}
func (s *TeamStore) GetMember(ctx context.Context, teamID, userID string) (*models.TeamMember, error) {
var m models.TeamMember
err := DB.QueryRowContext(ctx, `
SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.joined_at,
u.email, COALESCE(u.display_name, ''), u.username, u.role as user_role
FROM team_members tm
JOIN users u ON u.id = tm.user_id
WHERE tm.team_id = ? AND tm.user_id = ?`, teamID, userID).Scan(
&m.ID, &m.TeamID, &m.UserID, &m.Role, &m.JoinedAt,
&m.Email, &m.DisplayName, &m.Username, &m.UserRole)
if err != nil {
return nil, err
}
return &m, nil
}
// GetUserTeamIDs returns all team IDs a user belongs to.
func (s *TeamStore) GetUserTeamIDs(ctx context.Context, userID string) ([]string, error) {
rows, err := DB.QueryContext(ctx,
"SELECT team_id FROM team_members WHERE user_id = ?", userID)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, rows.Err()
}
func (s *TeamStore) IsTeamAdmin(ctx context.Context, teamID, userID string) (bool, error) {
var role string
err := DB.QueryRowContext(ctx,
"SELECT role FROM team_members WHERE team_id = ? AND user_id = ?",
teamID, userID).Scan(&role)
if err == sql.ErrNoRows {
return false, nil
}
if err != nil {
return false, err
}
return role == models.TeamRoleAdmin, nil
}
func (s *TeamStore) IsMember(ctx context.Context, teamID, userID string) (bool, error) {
var exists bool
err := DB.QueryRowContext(ctx,
"SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = ? AND user_id = ?)",
teamID, userID).Scan(&exists)
return exists, err
}
// unused but keeping for reference
var _ = fmt.Sprintf
// ── CS1 additions (v0.29.0) ─────────────────────────────────────────────
func (s *TeamStore) Exists(ctx context.Context, teamID string) (bool, error) {
var count int
err := DB.QueryRowContext(ctx,
`SELECT COUNT(*) FROM teams WHERE id = ?`, teamID).Scan(&count)
return count > 0, err
}
func (s *TeamStore) UpdateMemberRoleByID(ctx context.Context, memberID, teamID, role string) (int64, error) {
res, err := DB.ExecContext(ctx,
`UPDATE team_members SET role = ? WHERE id = ? AND team_id = ?`,
role, memberID, teamID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *TeamStore) DeleteMemberByID(ctx context.Context, memberID, teamID string) (int64, error) {
res, err := DB.ExecContext(ctx,
`DELETE FROM team_members WHERE id = ? AND team_id = ?`,
memberID, teamID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
func (s *TeamStore) ListTeamAuditActions(ctx context.Context, teamID string) ([]string, error) {
rows, err := DB.QueryContext(ctx, `
SELECT DISTINCT al.action
FROM audit_log al
WHERE al.actor_id IN (SELECT user_id FROM team_members WHERE team_id = ?)
ORDER BY al.action
`, teamID)
if err != nil {
return nil, err
}
defer rows.Close()
var actions []string
for rows.Next() {
var a string
if err := rows.Scan(&a); err != nil {
return nil, err
}
actions = append(actions, a)
}
if actions == nil {
actions = []string{}
}
return actions, rows.Err()
}
// ── CS5b additions (v0.29.0) ────────────────────────────────────────────
func (s *TeamStore) GetFirstTeamIDForUser(ctx context.Context, userID string) (string, error) {
var teamID string
err := DB.QueryRowContext(ctx,
`SELECT team_id FROM team_members WHERE user_id = ? LIMIT 1`, userID).Scan(&teamID)
if err != nil {
return "", nil
}
return teamID, nil
}
// ── CS6 additions (v0.29.0) ────────────────────────────────────────────
func (s *TeamStore) AddMemberReturningID(ctx context.Context, teamID, userID, role string) (string, error) {
id := store.NewID()
_, err := DB.ExecContext(ctx, `
INSERT INTO team_members (id, team_id, user_id, role)
VALUES (?, ?, ?, ?)`, id, teamID, userID, role)
return id, err
}
func (s *TeamStore) HasPrivateProviderRequirement(ctx context.Context, userID string) (bool, error) {
var has bool
err := DB.QueryRowContext(ctx, `
SELECT EXISTS(
SELECT 1 FROM team_members tm
JOIN teams t ON t.id = tm.team_id
WHERE tm.user_id = ?
AND t.is_active = 1
AND json_extract(t.settings, '$.require_private_providers') = 'true'
)`, userID).Scan(&has)
return has, err
}
func (s *TeamStore) MergeSettings(ctx context.Context, teamID, settingsJSON string) error {
_, err := DB.ExecContext(ctx,
`UPDATE teams SET settings = json_patch(COALESCE(settings, '{}'), ?) WHERE id = ?`,
settingsJSON, teamID)
return err
}