Changeset 0.31.2 (#205)
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
slug VARCHAR(200) NOT NULL DEFAULT '' UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL DEFAULT '' UNIQUE,
|
||||
description TEXT DEFAULT '',
|
||||
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
is_active INTEGER DEFAULT 1,
|
||||
|
||||
@@ -105,6 +105,7 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
|
||||
|
||||
team := &models.Team{
|
||||
Name: req.Name,
|
||||
Slug: slugify(req.Name),
|
||||
Description: req.Description,
|
||||
CreatedBy: adminID,
|
||||
IsActive: true,
|
||||
@@ -118,7 +119,7 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"id": team.ID, "name": team.Name})
|
||||
c.JSON(http.StatusCreated, gin.H{"id": team.ID, "name": team.Name, "slug": team.Slug})
|
||||
AuditLog(h.stores.Audit, c, "team.create", "team", team.ID, map[string]interface{}{"name": team.Name})
|
||||
}
|
||||
|
||||
@@ -155,6 +156,7 @@ func (h *TeamHandler) UpdateTeam(c *gin.Context) {
|
||||
fields := map[string]interface{}{}
|
||||
if req.Name != nil {
|
||||
fields["name"] = *req.Name
|
||||
fields["slug"] = slugify(*req.Name)
|
||||
}
|
||||
if req.Description != nil {
|
||||
fields["description"] = *req.Description
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -23,6 +24,24 @@ func NewWorkflowEntryHandler(stores store.Stores) *WorkflowEntryHandler {
|
||||
return &WorkflowEntryHandler{stores: stores}
|
||||
}
|
||||
|
||||
// resolveTeamScope converts a URL scope parameter to a team ID.
|
||||
// Accepts "global" (returns nil), a UUID (passed through), or a team slug (looked up).
|
||||
func resolveTeamScope(ctx context.Context, teams store.TeamStore, scope string) *string {
|
||||
if scope == "global" {
|
||||
return nil
|
||||
}
|
||||
// UUID format: 36 chars with dashes at 8,13,18,23
|
||||
if len(scope) == 36 && scope[8] == '-' && scope[13] == '-' {
|
||||
return &scope
|
||||
}
|
||||
// Try team slug lookup
|
||||
t, err := teams.GetBySlug(ctx, scope)
|
||||
if err == nil && t != nil {
|
||||
return &t.ID
|
||||
}
|
||||
return &scope // fall through — will 404 downstream
|
||||
}
|
||||
|
||||
// StartVisitor creates an anonymous session + workflow instance for a visitor.
|
||||
// POST /api/v1/workflow-entry/:scope/:slug
|
||||
func (h *WorkflowEntryHandler) StartVisitor(c *gin.Context) {
|
||||
@@ -30,10 +49,7 @@ func (h *WorkflowEntryHandler) StartVisitor(c *gin.Context) {
|
||||
scope := c.Param("scope")
|
||||
slug := c.Param("slug")
|
||||
|
||||
var teamID *string
|
||||
if scope != "global" {
|
||||
teamID = &scope
|
||||
}
|
||||
teamID := resolveTeamScope(ctx, h.stores.Teams, scope)
|
||||
|
||||
wf, err := h.stores.Workflows.GetBySlug(ctx, teamID, slug)
|
||||
if err != nil || wf == nil {
|
||||
|
||||
157
server/handlers/workflow_team.go
Normal file
157
server/handlers/workflow_team.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
)
|
||||
|
||||
// ── Team-Scoped Workflow Wrappers (v0.31.2) ────────────────
|
||||
//
|
||||
// Thin wrappers that enforce team ownership before delegating
|
||||
// to the existing WorkflowHandler methods. Follows the same
|
||||
// pattern as CreateTeamTask / ListTeamTasks.
|
||||
|
||||
// requireTeamWorkflow loads the workflow by :id and verifies
|
||||
// its team_id matches :teamId. Returns false (and writes an
|
||||
// HTTP error) if the check fails; true if the caller may proceed.
|
||||
func (h *WorkflowHandler) requireTeamWorkflow(c *gin.Context) bool {
|
||||
teamID := c.Param("teamId")
|
||||
wfID := c.Param("id")
|
||||
w, err := h.stores.Workflows.GetByID(c.Request.Context(), wfID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "workflow not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load workflow"})
|
||||
}
|
||||
return false
|
||||
}
|
||||
if w.TeamID == nil || *w.TeamID != teamID {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "workflow does not belong to this team"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ── Workflow CRUD ───────────────────────────
|
||||
|
||||
// ListTeamWorkflows returns workflows owned by the team.
|
||||
// GET /api/v1/teams/:teamId/workflows
|
||||
func (h *WorkflowHandler) ListTeamWorkflows(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
result, err := h.stores.Workflows.ListForTeam(c.Request.Context(), teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list workflows"})
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
result = []models.Workflow{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": result})
|
||||
}
|
||||
|
||||
// CreateTeamWorkflow creates a workflow scoped to the team.
|
||||
// POST /api/v1/teams/:teamId/workflows
|
||||
func (h *WorkflowHandler) CreateTeamWorkflow(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
c.Set("force_team_id", teamID)
|
||||
h.Create(c)
|
||||
}
|
||||
|
||||
// GetTeamWorkflow returns a team workflow with stages.
|
||||
// GET /api/v1/teams/:teamId/workflows/:id
|
||||
func (h *WorkflowHandler) GetTeamWorkflow(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.Get(c)
|
||||
}
|
||||
|
||||
// UpdateTeamWorkflow patches a team workflow.
|
||||
// PATCH /api/v1/teams/:teamId/workflows/:id
|
||||
func (h *WorkflowHandler) UpdateTeamWorkflow(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.Update(c)
|
||||
}
|
||||
|
||||
// DeleteTeamWorkflow deletes a team workflow.
|
||||
// DELETE /api/v1/teams/:teamId/workflows/:id
|
||||
func (h *WorkflowHandler) DeleteTeamWorkflow(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.Delete(c)
|
||||
}
|
||||
|
||||
// ── Stage CRUD ──────────────────────────────
|
||||
|
||||
// ListTeamWorkflowStages returns stages for a team workflow.
|
||||
// GET /api/v1/teams/:teamId/workflows/:id/stages
|
||||
func (h *WorkflowHandler) ListTeamWorkflowStages(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.ListStages(c)
|
||||
}
|
||||
|
||||
// CreateTeamWorkflowStage adds a stage to a team workflow.
|
||||
// POST /api/v1/teams/:teamId/workflows/:id/stages
|
||||
func (h *WorkflowHandler) CreateTeamWorkflowStage(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.CreateStage(c)
|
||||
}
|
||||
|
||||
// UpdateTeamWorkflowStage updates a stage on a team workflow.
|
||||
// PUT /api/v1/teams/:teamId/workflows/:id/stages/:sid
|
||||
func (h *WorkflowHandler) UpdateTeamWorkflowStage(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.UpdateStage(c)
|
||||
}
|
||||
|
||||
// DeleteTeamWorkflowStage removes a stage from a team workflow.
|
||||
// DELETE /api/v1/teams/:teamId/workflows/:id/stages/:sid
|
||||
func (h *WorkflowHandler) DeleteTeamWorkflowStage(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.DeleteStage(c)
|
||||
}
|
||||
|
||||
// ReorderTeamWorkflowStages reorders stages on a team workflow.
|
||||
// PATCH /api/v1/teams/:teamId/workflows/:id/stages/reorder
|
||||
func (h *WorkflowHandler) ReorderTeamWorkflowStages(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.ReorderStages(c)
|
||||
}
|
||||
|
||||
// ── Publish / Version ───────────────────────
|
||||
|
||||
// PublishTeamWorkflow publishes a team workflow version.
|
||||
// POST /api/v1/teams/:teamId/workflows/:id/publish
|
||||
func (h *WorkflowHandler) PublishTeamWorkflow(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.Publish(c)
|
||||
}
|
||||
|
||||
// GetTeamWorkflowVersion returns a version snapshot for a team workflow.
|
||||
// GET /api/v1/teams/:teamId/workflows/:id/versions/:version
|
||||
func (h *WorkflowHandler) GetTeamWorkflowVersion(c *gin.Context) {
|
||||
if !h.requireTeamWorkflow(c) {
|
||||
return
|
||||
}
|
||||
h.GetVersion(c)
|
||||
}
|
||||
@@ -106,6 +106,12 @@ func (h *WorkflowHandler) Create(c *gin.Context) {
|
||||
|
||||
w.CreatedBy = c.GetString("user_id")
|
||||
|
||||
// v0.31.2: Team-scoped workflow creation (set by CreateTeamWorkflow)
|
||||
if forceTeam, ok := c.Get("force_team_id"); ok {
|
||||
teamID := forceTeam.(string)
|
||||
w.TeamID = &teamID
|
||||
}
|
||||
|
||||
if err := h.stores.Workflows.Create(c.Request.Context(), &w); err != nil {
|
||||
if strings.Contains(err.Error(), "unique") || strings.Contains(err.Error(), "UNIQUE") {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "slug already exists in this scope"})
|
||||
|
||||
@@ -500,7 +500,7 @@ func main() {
|
||||
log.Printf(" 🔑 Auth mode: %s", authMode)
|
||||
|
||||
authH := handlers.NewAuthHandler(cfg, stores, uekCache, authProvider)
|
||||
authLimiter := middleware.NewRateLimiter(5, 8)
|
||||
authLimiter := middleware.NewRateLimiter(5, 5)
|
||||
|
||||
api := base.Group("/api/v1")
|
||||
{
|
||||
@@ -990,6 +990,21 @@ func main() {
|
||||
teamScoped.DELETE("/tasks/:id", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.Delete)
|
||||
teamScoped.POST("/tasks/:id/run", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.RunNow)
|
||||
teamScoped.POST("/tasks/:id/kill", middleware.RequirePermission(auth.PermTaskCreate, stores), teamTaskH.KillRun)
|
||||
|
||||
// Team workflows — self-service CRUD (v0.31.2)
|
||||
teamWfH := handlers.NewWorkflowHandler(stores)
|
||||
teamScoped.GET("/workflows", teamWfH.ListTeamWorkflows)
|
||||
teamScoped.POST("/workflows", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.CreateTeamWorkflow)
|
||||
teamScoped.GET("/workflows/:id", teamWfH.GetTeamWorkflow)
|
||||
teamScoped.PATCH("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.UpdateTeamWorkflow)
|
||||
teamScoped.DELETE("/workflows/:id", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.DeleteTeamWorkflow)
|
||||
teamScoped.GET("/workflows/:id/stages", teamWfH.ListTeamWorkflowStages)
|
||||
teamScoped.POST("/workflows/:id/stages", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.CreateTeamWorkflowStage)
|
||||
teamScoped.PUT("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.UpdateTeamWorkflowStage)
|
||||
teamScoped.DELETE("/workflows/:id/stages/:sid", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.DeleteTeamWorkflowStage)
|
||||
teamScoped.PATCH("/workflows/:id/stages/reorder", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.ReorderTeamWorkflowStages)
|
||||
teamScoped.POST("/workflows/:id/publish", middleware.RequirePermission(auth.PermWorkflowCreate, stores), teamWfH.PublishTeamWorkflow)
|
||||
teamScoped.GET("/workflows/:id/versions/:version", teamWfH.GetTeamWorkflowVersion)
|
||||
}
|
||||
|
||||
// Team task viewing for all members (v0.27.5)
|
||||
|
||||
@@ -92,6 +92,7 @@ type User struct {
|
||||
type Team struct {
|
||||
BaseModel
|
||||
Name string `json:"name" db:"name"`
|
||||
Slug string `json:"slug" db:"slug"`
|
||||
Description string `json:"description,omitempty" db:"description"`
|
||||
CreatedBy string `json:"created_by" db:"created_by"`
|
||||
IsActive bool `json:"is_active" db:"is_active"`
|
||||
|
||||
@@ -700,10 +700,23 @@ func (e *Engine) RenderWorkflowLanding() gin.HandlerFunc {
|
||||
scope := c.Param("id")
|
||||
slug := c.Param("slug")
|
||||
|
||||
// Resolve scope to team_id
|
||||
// Resolve scope to team_id (v0.31.2: supports team slug)
|
||||
var teamID *string
|
||||
if scope != "global" {
|
||||
teamID = &scope
|
||||
// UUID format check: 36 chars with dashes at standard positions
|
||||
if len(scope) == 36 && scope[8] == '-' && scope[13] == '-' {
|
||||
teamID = &scope
|
||||
} else if e.stores.Teams != nil {
|
||||
// Try team slug lookup
|
||||
t, err := e.stores.Teams.GetBySlug(ctx, scope)
|
||||
if err == nil && t != nil {
|
||||
teamID = &t.ID
|
||||
} else {
|
||||
teamID = &scope // fall through — will 404 downstream
|
||||
}
|
||||
} else {
|
||||
teamID = &scope
|
||||
}
|
||||
}
|
||||
|
||||
if e.stores.Workflows == nil {
|
||||
|
||||
@@ -374,6 +374,7 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
<button class="admin-tab active" data-ttab="members" onclick="sb.call('UI.switchTeamTab','members')">Members</button>
|
||||
<button class="admin-tab" data-ttab="providers" onclick="sb.call('UI.switchTeamTab','providers')">Providers</button>
|
||||
<button class="admin-tab" data-ttab="personas" onclick="sb.call('UI.switchTeamTab','personas')">Personas</button>
|
||||
<button class="admin-tab" data-ttab="workflows" onclick="sb.call('UI.switchTeamTab','workflows')">Workflows</button>
|
||||
<button class="admin-tab" data-ttab="knowledge" onclick="sb.call('UI.switchTeamTab','knowledge')">Knowledge</button>
|
||||
<button class="admin-tab" data-ttab="groups" onclick="sb.call('UI.switchTeamTab','groups')">Groups</button>
|
||||
<button class="admin-tab" data-ttab="settings" onclick="sb.call('UI.switchTeamTab','settings')">Settings</button>
|
||||
@@ -418,6 +419,9 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
<div id="teamKBContent"></div>
|
||||
</div>
|
||||
|
||||
{{/* Workflows (v0.31.2) */}}
|
||||
<div id="teamTabWorkflows" class="team-tab-content" style="display:none;"></div>
|
||||
|
||||
{{/* Groups */}}
|
||||
<div id="teamTabGroups" class="team-tab-content" style="display:none;">
|
||||
<div id="settingsTeamGroups"></div>
|
||||
@@ -524,6 +528,7 @@ window.addEventListener('unhandledrejection', function(e) {
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/admin-handlers.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-api.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-admin.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-team-admin.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/workflow-queue.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/task-sidebar.js?v={{$.Version}}"></script>
|
||||
<script type="module" nonce="{{$.CSPNonce}}" src="{{$.BasePath}}/js/app.js?v={{$.Version}}"></script>
|
||||
|
||||
@@ -340,6 +340,7 @@ type UserSearchResult struct {
|
||||
type TeamStore interface {
|
||||
Create(ctx context.Context, t *models.Team) error
|
||||
GetByID(ctx context.Context, id string) (*models.Team, error)
|
||||
GetBySlug(ctx context.Context, slug 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)
|
||||
|
||||
@@ -16,10 +16,10 @@ func NewTeamStore() *TeamStore { return &TeamStore{} }
|
||||
func (s *TeamStore) Create(ctx context.Context, t *models.Team) error {
|
||||
settingsJSON := ToJSON(t.Settings)
|
||||
return DB.QueryRowContext(ctx, `
|
||||
INSERT INTO teams (name, description, created_by, is_active, settings)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO teams (name, slug, description, created_by, is_active, settings)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, created_at, updated_at`,
|
||||
t.Name, t.Description, t.CreatedBy, t.IsActive, settingsJSON,
|
||||
t.Name, t.Slug, t.Description, t.CreatedBy, t.IsActive, settingsJSON,
|
||||
).Scan(&t.ID, &t.CreatedAt, &t.UpdatedAt)
|
||||
}
|
||||
|
||||
@@ -27,10 +27,27 @@ 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, description, created_by, is_active, settings, created_at, updated_at,
|
||||
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 = $1`, id).Scan(
|
||||
&t.ID, &t.Name, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, &t.CreatedAt, &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 = $1`, slug).Scan(
|
||||
&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MemberCount,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -64,7 +81,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, description, created_by, is_active, settings, created_at, updated_at,
|
||||
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 {
|
||||
@@ -76,7 +93,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.Description, &t.CreatedBy, &t.IsActive,
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MemberCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,7 +106,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.description, t.created_by, t.is_active,
|
||||
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
|
||||
@@ -106,7 +123,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.Description, &t.CreatedBy, &t.IsActive,
|
||||
err := rows.Scan(&t.ID, &t.Name, &t.Slug, &t.Description, &t.CreatedBy, &t.IsActive,
|
||||
&settingsJSON, &t.CreatedAt, &t.UpdatedAt, &t.MyRole, &t.MemberCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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, description, created_by, is_active, settings, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
t.ID, t.Name, t.Description, t.CreatedBy, t.IsActive, settingsJSON,
|
||||
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
|
||||
@@ -34,10 +34,27 @@ 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, description, created_by, is_active, settings, created_at, updated_at,
|
||||
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.Description, &t.CreatedBy, &t.IsActive,
|
||||
&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 {
|
||||
@@ -71,7 +88,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, description, created_by, is_active, settings, created_at, updated_at,
|
||||
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 {
|
||||
@@ -83,7 +100,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.Description, &t.CreatedBy, &t.IsActive,
|
||||
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
|
||||
@@ -96,7 +113,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.description, t.created_by, t.is_active,
|
||||
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
|
||||
@@ -113,7 +130,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.Description, &t.CreatedBy, &t.IsActive,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user