Changeset 0.26.0 (#165)
This commit is contained in:
@@ -3,6 +3,7 @@ package sqlite
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
@@ -95,6 +96,21 @@ func (s *SessionStore) Delete(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExpired removes sessions created before olderThan whose channel
|
||||
// has no messages. Returns the count of deleted rows.
|
||||
func (s *SessionStore) DeleteExpired(ctx context.Context, olderThan time.Time) (int64, error) {
|
||||
res, err := DB.ExecContext(ctx, `
|
||||
DELETE FROM session_participants
|
||||
WHERE created_at < ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM messages WHERE messages.channel_id = session_participants.channel_id
|
||||
)`, olderThan)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
func nullText(s string) interface{} {
|
||||
if s == "" {
|
||||
return nil
|
||||
|
||||
@@ -40,5 +40,6 @@ func NewStores(db *sql.DB) store.Stores {
|
||||
CapOverrides: NewCapOverrideStore(),
|
||||
RoutingPolicies: NewRoutingPolicyStore(),
|
||||
Sessions: NewSessionStore(),
|
||||
Workflows: NewWorkflowStore(),
|
||||
}
|
||||
}
|
||||
|
||||
328
server/store/sqlite/workflows.go
Normal file
328
server/store/sqlite/workflows.go
Normal file
@@ -0,0 +1,328 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/models"
|
||||
"git.gobha.me/xcaliber/chat-switchboard/store"
|
||||
)
|
||||
|
||||
// WorkflowStore implements store.WorkflowStore for SQLite.
|
||||
type WorkflowStore struct{}
|
||||
|
||||
func NewWorkflowStore() *WorkflowStore { return &WorkflowStore{} }
|
||||
|
||||
// ── Workflow CRUD ───────────────────────────
|
||||
|
||||
func (s *WorkflowStore) Create(ctx context.Context, w *models.Workflow) error {
|
||||
w.ID = store.NewID()
|
||||
now := time.Now().UTC()
|
||||
w.CreatedAt = now
|
||||
w.UpdatedAt = now
|
||||
w.Version = 1
|
||||
branding := jsonOrEmpty(w.Branding)
|
||||
retention := jsonOrDefault(w.Retention, `{"mode":"archive"}`)
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO workflows (id, team_id, name, slug, description, branding, entry_mode,
|
||||
is_active, version, on_complete, retention, created_by, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
w.ID, w.TeamID, w.Name, w.Slug, w.Description, branding, w.EntryMode,
|
||||
boolToInt(w.IsActive), w.Version, jsonOrNull(w.OnComplete), retention,
|
||||
w.CreatedBy, w.CreatedAt.Format(time.RFC3339), w.UpdatedAt.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) GetByID(ctx context.Context, id string) (*models.Workflow, error) {
|
||||
return s.scanOne(ctx, `
|
||||
SELECT id, team_id, name, slug, description, branding, entry_mode, is_active,
|
||||
version, on_complete, retention, created_by, created_at, updated_at
|
||||
FROM workflows WHERE id = ?`, id)
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) GetBySlug(ctx context.Context, teamID *string, slug string) (*models.Workflow, error) {
|
||||
if teamID != nil {
|
||||
return s.scanOne(ctx, `
|
||||
SELECT id, team_id, name, slug, description, branding, entry_mode, is_active,
|
||||
version, on_complete, retention, created_by, created_at, updated_at
|
||||
FROM workflows WHERE team_id = ? AND slug = ?`, *teamID, slug)
|
||||
}
|
||||
return s.scanOne(ctx, `
|
||||
SELECT id, team_id, name, slug, description, branding, entry_mode, is_active,
|
||||
version, on_complete, retention, created_by, created_at, updated_at
|
||||
FROM workflows WHERE team_id IS NULL AND slug = ?`, slug)
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) Update(ctx context.Context, id string, patch models.WorkflowPatch) error {
|
||||
sets := []string{}
|
||||
args := []interface{}{}
|
||||
|
||||
if patch.Name != nil {
|
||||
sets = append(sets, "name = ?")
|
||||
args = append(args, *patch.Name)
|
||||
}
|
||||
if patch.Description != nil {
|
||||
sets = append(sets, "description = ?")
|
||||
args = append(args, *patch.Description)
|
||||
}
|
||||
if patch.Branding != nil {
|
||||
sets = append(sets, "branding = ?")
|
||||
args = append(args, string(*patch.Branding))
|
||||
}
|
||||
if patch.EntryMode != nil {
|
||||
sets = append(sets, "entry_mode = ?")
|
||||
args = append(args, *patch.EntryMode)
|
||||
}
|
||||
if patch.IsActive != nil {
|
||||
sets = append(sets, "is_active = ?")
|
||||
args = append(args, boolToInt(*patch.IsActive))
|
||||
}
|
||||
if patch.OnComplete != nil {
|
||||
sets = append(sets, "on_complete = ?")
|
||||
args = append(args, string(*patch.OnComplete))
|
||||
}
|
||||
if patch.Retention != nil {
|
||||
sets = append(sets, "retention = ?")
|
||||
args = append(args, string(*patch.Retention))
|
||||
}
|
||||
|
||||
if len(sets) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sets = append(sets, "version = version + 1")
|
||||
sets = append(sets, "updated_at = ?")
|
||||
args = append(args, time.Now().UTC().Format(time.RFC3339))
|
||||
|
||||
q := "UPDATE workflows SET "
|
||||
for i, s := range sets {
|
||||
if i > 0 {
|
||||
q += ", "
|
||||
}
|
||||
q += s
|
||||
}
|
||||
q += " WHERE id = ?"
|
||||
args = append(args, id)
|
||||
|
||||
_, err := DB.ExecContext(ctx, q, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) Delete(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM workflows WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) ListForTeam(ctx context.Context, teamID string) ([]models.Workflow, error) {
|
||||
return s.queryWorkflows(ctx, `
|
||||
SELECT id, team_id, name, slug, description, branding, entry_mode, is_active,
|
||||
version, on_complete, retention, created_by, created_at, updated_at
|
||||
FROM workflows WHERE team_id = ?
|
||||
ORDER BY name ASC`, teamID)
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) ListGlobal(ctx context.Context) ([]models.Workflow, error) {
|
||||
return s.queryWorkflows(ctx, `
|
||||
SELECT id, team_id, name, slug, description, branding, entry_mode, is_active,
|
||||
version, on_complete, retention, created_by, created_at, updated_at
|
||||
FROM workflows WHERE team_id IS NULL
|
||||
ORDER BY name ASC`)
|
||||
}
|
||||
|
||||
// ── Stages ──────────────────────────────────
|
||||
|
||||
func (s *WorkflowStore) CreateStage(ctx context.Context, st *models.WorkflowStage) error {
|
||||
st.ID = store.NewID()
|
||||
st.CreatedAt = time.Now().UTC()
|
||||
formTpl := jsonOrEmpty(st.FormTemplate)
|
||||
transRules := jsonOrEmpty(st.TransitionRules)
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO workflow_stages (id, workflow_id, ordinal, name, persona_id, assignment_team_id,
|
||||
form_template, history_mode, auto_transition, transition_rules, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
st.ID, st.WorkflowID, st.Ordinal, st.Name, st.PersonaID, st.AssignmentTeamID,
|
||||
formTpl, st.HistoryMode, boolToInt(st.AutoTransition), transRules,
|
||||
st.CreatedAt.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) ListStages(ctx context.Context, workflowID string) ([]models.WorkflowStage, error) {
|
||||
rows, err := DB.QueryContext(ctx, `
|
||||
SELECT id, workflow_id, ordinal, name, persona_id, assignment_team_id,
|
||||
form_template, history_mode, auto_transition, transition_rules, created_at
|
||||
FROM workflow_stages WHERE workflow_id = ?
|
||||
ORDER BY ordinal ASC`, workflowID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []models.WorkflowStage
|
||||
for rows.Next() {
|
||||
var st models.WorkflowStage
|
||||
var formTpl, transRules string
|
||||
var autoTrans int
|
||||
if err := rows.Scan(&st.ID, &st.WorkflowID, &st.Ordinal, &st.Name,
|
||||
&st.PersonaID, &st.AssignmentTeamID, &formTpl, &st.HistoryMode,
|
||||
&autoTrans, &transRules, &st.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.FormTemplate = json.RawMessage(formTpl)
|
||||
st.TransitionRules = json.RawMessage(transRules)
|
||||
st.AutoTransition = autoTrans != 0
|
||||
result = append(result, st)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) UpdateStage(ctx context.Context, st *models.WorkflowStage) error {
|
||||
formTpl := jsonOrEmpty(st.FormTemplate)
|
||||
transRules := jsonOrEmpty(st.TransitionRules)
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
UPDATE workflow_stages
|
||||
SET ordinal = ?, name = ?, persona_id = ?, assignment_team_id = ?,
|
||||
form_template = ?, history_mode = ?, auto_transition = ?, transition_rules = ?
|
||||
WHERE id = ?`,
|
||||
st.Ordinal, st.Name, st.PersonaID, st.AssignmentTeamID,
|
||||
formTpl, st.HistoryMode, boolToInt(st.AutoTransition), transRules, st.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) DeleteStage(ctx context.Context, id string) error {
|
||||
_, err := DB.ExecContext(ctx, `DELETE FROM workflow_stages WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) ReorderStages(ctx context.Context, workflowID string, orderedIDs []string) error {
|
||||
tx, err := DB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
for i, id := range orderedIDs {
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
UPDATE workflow_stages SET ordinal = ? WHERE id = ? AND workflow_id = ?`,
|
||||
i, id, workflowID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// ── Versions ────────────────────────────────
|
||||
|
||||
func (s *WorkflowStore) Publish(ctx context.Context, v *models.WorkflowVersion) error {
|
||||
v.ID = store.NewID()
|
||||
v.CreatedAt = time.Now().UTC()
|
||||
_, err := DB.ExecContext(ctx, `
|
||||
INSERT INTO workflow_versions (id, workflow_id, version_number, snapshot, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
v.ID, v.WorkflowID, v.VersionNumber, string(v.Snapshot),
|
||||
v.CreatedAt.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) GetVersion(ctx context.Context, workflowID string, versionNumber int) (*models.WorkflowVersion, error) {
|
||||
v := &models.WorkflowVersion{}
|
||||
var snapshot string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, workflow_id, version_number, snapshot, created_at
|
||||
FROM workflow_versions WHERE workflow_id = ? AND version_number = ?`,
|
||||
workflowID, versionNumber,
|
||||
).Scan(&v.ID, &v.WorkflowID, &v.VersionNumber, &snapshot, &v.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Snapshot = json.RawMessage(snapshot)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) GetLatestVersion(ctx context.Context, workflowID string) (*models.WorkflowVersion, error) {
|
||||
v := &models.WorkflowVersion{}
|
||||
var snapshot string
|
||||
err := DB.QueryRowContext(ctx, `
|
||||
SELECT id, workflow_id, version_number, snapshot, created_at
|
||||
FROM workflow_versions WHERE workflow_id = ?
|
||||
ORDER BY version_number DESC LIMIT 1`,
|
||||
workflowID,
|
||||
).Scan(&v.ID, &v.WorkflowID, &v.VersionNumber, &snapshot, &v.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.Snapshot = json.RawMessage(snapshot)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────
|
||||
|
||||
func (s *WorkflowStore) scanOne(ctx context.Context, q string, args ...interface{}) (*models.Workflow, error) {
|
||||
w := &models.Workflow{}
|
||||
var branding, retention string
|
||||
var onComplete sql.NullString
|
||||
var isActive int
|
||||
err := DB.QueryRowContext(ctx, q, args...).Scan(
|
||||
&w.ID, &w.TeamID, &w.Name, &w.Slug, &w.Description, &branding,
|
||||
&w.EntryMode, &isActive, &w.Version, &onComplete, &retention,
|
||||
&w.CreatedBy, &w.CreatedAt, &w.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.Branding = json.RawMessage(branding)
|
||||
w.Retention = json.RawMessage(retention)
|
||||
w.IsActive = isActive != 0
|
||||
if onComplete.Valid {
|
||||
w.OnComplete = json.RawMessage(onComplete.String)
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *WorkflowStore) queryWorkflows(ctx context.Context, q string, args ...interface{}) ([]models.Workflow, error) {
|
||||
rows, err := DB.QueryContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []models.Workflow
|
||||
for rows.Next() {
|
||||
var w models.Workflow
|
||||
var branding, retention string
|
||||
var onComplete sql.NullString
|
||||
var isActive int
|
||||
if err := rows.Scan(&w.ID, &w.TeamID, &w.Name, &w.Slug, &w.Description,
|
||||
&branding, &w.EntryMode, &isActive, &w.Version, &onComplete,
|
||||
&retention, &w.CreatedBy, &w.CreatedAt, &w.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.Branding = json.RawMessage(branding)
|
||||
w.Retention = json.RawMessage(retention)
|
||||
w.IsActive = isActive != 0
|
||||
if onComplete.Valid {
|
||||
w.OnComplete = json.RawMessage(onComplete.String)
|
||||
}
|
||||
result = append(result, w)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func jsonOrEmpty(b json.RawMessage) string {
|
||||
if len(b) == 0 {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func jsonOrDefault(b json.RawMessage, def string) string {
|
||||
if len(b) == 0 {
|
||||
return def
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func jsonOrNull(b json.RawMessage) interface{} {
|
||||
if len(b) == 0 || string(b) == "null" {
|
||||
return nil
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user