mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-04 13:04:38 +00:00
bdb60de7ae
- Update go.mod and Dockerfile to Go 1.26 - Apply `go fix ./...` stdlib modernizations across 170+ files - Add `go fix` to post-implementation checklist in CLAUDE.md - Fix go fix misapplied rewrite in loop_history.go
215 lines
5.8 KiB
Go
215 lines
5.8 KiB
Go
package pg
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
// PGBuiltinToolStore implements store.BuiltinToolStore backed by Postgres.
|
|
type PGBuiltinToolStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewPGBuiltinToolStore(db *sql.DB) *PGBuiltinToolStore {
|
|
return &PGBuiltinToolStore{db: db}
|
|
}
|
|
|
|
const builtinToolSelectCols = `name, display_name, description, category, enabled, settings, requires, metadata, created_at, updated_at`
|
|
|
|
func (s *PGBuiltinToolStore) List(ctx context.Context) ([]store.BuiltinToolDef, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT `+builtinToolSelectCols+` FROM builtin_tools ORDER BY category, name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.scanTools(rows)
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) ListEnabled(ctx context.Context) ([]store.BuiltinToolDef, error) {
|
|
rows, err := s.db.QueryContext(ctx,
|
|
`SELECT `+builtinToolSelectCols+` FROM builtin_tools WHERE enabled = true ORDER BY category, name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.scanTools(rows)
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) Get(ctx context.Context, name string) (*store.BuiltinToolDef, error) {
|
|
row := s.db.QueryRowContext(ctx,
|
|
`SELECT `+builtinToolSelectCols+` FROM builtin_tools WHERE name = $1`, name)
|
|
return s.scanTool(row)
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) GetSettings(ctx context.Context, name string) (json.RawMessage, error) {
|
|
var settings json.RawMessage
|
|
err := s.db.QueryRowContext(ctx,
|
|
`SELECT settings FROM builtin_tools WHERE name = $1`, name).Scan(&settings)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) Update(ctx context.Context, name string, updates map[string]any) error {
|
|
// Only allow updating enabled and settings
|
|
allowed := make(map[string]any)
|
|
if v, ok := updates["enabled"]; ok {
|
|
allowed["enabled"] = v
|
|
}
|
|
if v, ok := updates["settings"]; ok {
|
|
switch sv := v.(type) {
|
|
case json.RawMessage:
|
|
allowed["settings"] = []byte(sv)
|
|
case map[string]any:
|
|
b, err := json.Marshal(sv)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal settings: %w", err)
|
|
}
|
|
allowed["settings"] = b
|
|
case []byte:
|
|
allowed["settings"] = sv
|
|
case string:
|
|
allowed["settings"] = []byte(sv)
|
|
}
|
|
}
|
|
if len(allowed) == 0 {
|
|
return nil
|
|
}
|
|
allowed["updated_at"] = time.Now()
|
|
|
|
// Build UPDATE with name as PK (not UUID)
|
|
var setClauses []string
|
|
var args []any
|
|
i := 1
|
|
for col, val := range allowed {
|
|
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, i))
|
|
args = append(args, val)
|
|
i++
|
|
}
|
|
args = append(args, name)
|
|
q := fmt.Sprintf("UPDATE builtin_tools SET %s WHERE name = $%d", strings.Join(setClauses, ", "), i)
|
|
_, err := s.db.ExecContext(ctx, q, args...)
|
|
return err
|
|
}
|
|
|
|
// Seed inserts or updates builtin tool definitions.
|
|
// Preserves user-customized enabled and settings values across upgrades.
|
|
func (s *PGBuiltinToolStore) Seed(ctx context.Context, tools []store.BuiltinToolDef) error {
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
stmt, err := tx.PrepareContext(ctx,
|
|
`INSERT INTO builtin_tools (name, display_name, description, category, enabled, settings, requires, metadata, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9)
|
|
ON CONFLICT (name) DO UPDATE SET
|
|
display_name = EXCLUDED.display_name,
|
|
description = EXCLUDED.description,
|
|
category = EXCLUDED.category,
|
|
requires = EXCLUDED.requires,
|
|
metadata = EXCLUDED.metadata,
|
|
settings = CASE
|
|
WHEN builtin_tools.settings IS NULL OR builtin_tools.settings::text IN ('{}', 'null')
|
|
THEN EXCLUDED.settings
|
|
ELSE builtin_tools.settings
|
|
END,
|
|
updated_at = EXCLUDED.updated_at`)
|
|
if err != nil {
|
|
return fmt.Errorf("prepare seed stmt: %w", err)
|
|
}
|
|
defer stmt.Close()
|
|
|
|
now := time.Now()
|
|
for _, t := range tools {
|
|
settings := t.Settings
|
|
if settings == nil {
|
|
settings = json.RawMessage("{}")
|
|
}
|
|
metadata := t.Metadata
|
|
if metadata == nil {
|
|
metadata = json.RawMessage("{}")
|
|
}
|
|
_, err := stmt.ExecContext(ctx,
|
|
t.Name, t.DisplayName, t.Description, t.Category,
|
|
t.Enabled, []byte(settings), pqStringArray(t.Requires), []byte(metadata), now,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("seed tool %s: %w", t.Name, err)
|
|
}
|
|
}
|
|
|
|
// Reconcile: remove stale entries not in the current seed list
|
|
names := make([]string, len(tools))
|
|
for i, t := range tools {
|
|
names[i] = t.Name
|
|
}
|
|
if _, err := tx.ExecContext(ctx,
|
|
`DELETE FROM builtin_tools WHERE name != ALL($1)`, pqStringArray(names)); err != nil {
|
|
return fmt.Errorf("reconcile stale builtin tools: %w", err)
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) scanTool(row *sql.Row) (*store.BuiltinToolDef, error) {
|
|
var def store.BuiltinToolDef
|
|
var settings []byte
|
|
var requires []byte
|
|
var metadata []byte
|
|
|
|
err := row.Scan(
|
|
&def.Name, &def.DisplayName, &def.Description, &def.Category,
|
|
&def.Enabled, &settings, &requires, &metadata, &def.CreatedAt, &def.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if settings != nil {
|
|
def.Settings = json.RawMessage(settings)
|
|
}
|
|
if metadata != nil {
|
|
def.Metadata = json.RawMessage(metadata)
|
|
}
|
|
scanStringArray(requires, &def.Requires)
|
|
|
|
return &def, nil
|
|
}
|
|
|
|
func (s *PGBuiltinToolStore) scanTools(rows *sql.Rows) ([]store.BuiltinToolDef, error) {
|
|
defer rows.Close()
|
|
var result []store.BuiltinToolDef
|
|
for rows.Next() {
|
|
var def store.BuiltinToolDef
|
|
var settings []byte
|
|
var requires []byte
|
|
var metadata []byte
|
|
|
|
if err := rows.Scan(
|
|
&def.Name, &def.DisplayName, &def.Description, &def.Category,
|
|
&def.Enabled, &settings, &requires, &metadata, &def.CreatedAt, &def.UpdatedAt,
|
|
); err != nil {
|
|
continue
|
|
}
|
|
|
|
if settings != nil {
|
|
def.Settings = json.RawMessage(settings)
|
|
}
|
|
if metadata != nil {
|
|
def.Metadata = json.RawMessage(metadata)
|
|
}
|
|
scanStringArray(requires, &def.Requires)
|
|
|
|
result = append(result, def)
|
|
}
|
|
return result, nil
|
|
}
|