405 lines
12 KiB
Go
405 lines
12 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"inbox/internal/domain/role"
|
|
)
|
|
|
|
func (s *Store) GetRole(ctx context.Context, name string) (role.Definition, error) {
|
|
row := s.db.QueryRowContext(ctx, `
|
|
SELECT name, title, executor_kind, description, is_enabled, is_builtin, sort_order, created_at, updated_at
|
|
FROM roles
|
|
WHERE name = ?
|
|
`, name)
|
|
return scanRole(row)
|
|
}
|
|
|
|
func (s *Store) ListRoles(ctx context.Context) ([]role.Definition, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT name, title, executor_kind, description, is_enabled, is_builtin, sort_order, created_at, updated_at
|
|
FROM roles
|
|
ORDER BY sort_order, name
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list roles: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []role.Definition
|
|
for rows.Next() {
|
|
item, err := scanRole(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate roles: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Store) UpsertRole(ctx context.Context, value role.Definition, changedBy string) (role.Definition, error) {
|
|
_ = changedBy
|
|
value = role.NormalizeDefinition(value)
|
|
if err := value.Validate(); err != nil {
|
|
return role.Definition{}, err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return role.Definition{}, fmt.Errorf("begin upsert role: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
before, err := getRoleTx(ctx, tx, value.Name)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return role.Definition{}, err
|
|
}
|
|
now := s.now()
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
value.CreatedAt = now
|
|
} else {
|
|
value.CreatedAt = before.CreatedAt
|
|
}
|
|
value.UpdatedAt = now
|
|
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO roles(name, title, executor_kind, description, is_enabled, is_builtin, sort_order, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
title = excluded.title,
|
|
executor_kind = excluded.executor_kind,
|
|
description = excluded.description,
|
|
is_enabled = excluded.is_enabled,
|
|
is_builtin = excluded.is_builtin,
|
|
sort_order = excluded.sort_order,
|
|
updated_at = excluded.updated_at
|
|
`,
|
|
value.Name,
|
|
value.Title,
|
|
string(value.ExecutorKind),
|
|
value.Description,
|
|
boolToInt(value.IsEnabled),
|
|
boolToInt(value.IsBuiltin),
|
|
value.SortOrder,
|
|
value.CreatedAt,
|
|
value.UpdatedAt,
|
|
); err != nil {
|
|
return role.Definition{}, fmt.Errorf("upsert role %q: %w", value.Name, err)
|
|
}
|
|
after, err := getRoleTx(ctx, tx, value.Name)
|
|
if err != nil {
|
|
return role.Definition{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return role.Definition{}, fmt.Errorf("commit upsert role: %w", err)
|
|
}
|
|
return after, nil
|
|
}
|
|
|
|
func (s *Store) UpsertRolePrompt(ctx context.Context, value role.Prompt, changedBy string) (role.Prompt, error) {
|
|
if err := value.Validate(); err != nil {
|
|
return role.Prompt{}, err
|
|
}
|
|
return upsertVersionedConfig(ctx, s, value, changedBy, versionedConfigUpsertSpec[role.Prompt]{
|
|
entityName: "role prompt",
|
|
idKind: "role-prompt",
|
|
loadTx: func(ctx context.Context, tx *sql.Tx) (role.Prompt, error) {
|
|
return getRolePromptTx(ctx, tx, value.RoleName, value.WorkspaceID, value.PromptKind)
|
|
},
|
|
current: func(item role.Prompt) versionedConfigCurrent {
|
|
return versionedConfigCurrent{
|
|
ID: item.ID,
|
|
Version: item.Version,
|
|
CreatedAt: item.CreatedAt,
|
|
}
|
|
},
|
|
applyMetadata: func(item *role.Prompt, meta versionedConfigMetadata) {
|
|
item.ID = meta.ID
|
|
item.Version = meta.Version
|
|
item.CreatedAt = meta.CreatedAt
|
|
item.UpdatedAt = meta.UpdatedAt
|
|
},
|
|
writeTx: func(ctx context.Context, tx *sql.Tx, item role.Prompt) error {
|
|
_, err := tx.ExecContext(ctx, `
|
|
INSERT INTO role_prompts(id, role_name, workspace_id, prompt_kind, content_markdown, version, updated_by, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
content_markdown = excluded.content_markdown,
|
|
version = excluded.version,
|
|
updated_by = excluded.updated_by,
|
|
updated_at = excluded.updated_at
|
|
`,
|
|
item.ID,
|
|
item.RoleName,
|
|
nullableString(item.WorkspaceID),
|
|
string(item.PromptKind),
|
|
item.ContentMarkdown,
|
|
item.Version,
|
|
coalesceString(changedBy, item.UpdatedBy),
|
|
item.CreatedAt,
|
|
item.UpdatedAt,
|
|
)
|
|
return err
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Store) ListRolePrompts(ctx context.Context, roleName string) ([]role.Prompt, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT id, role_name, workspace_id, prompt_kind, content_markdown, version, updated_by, created_at, updated_at
|
|
FROM role_prompts
|
|
WHERE role_name = ?
|
|
ORDER BY prompt_kind, workspace_id, updated_at
|
|
`, roleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list role prompts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []role.Prompt
|
|
for rows.Next() {
|
|
item, err := scanRolePrompt(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate role prompts: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *Store) UpsertRoleConfig(ctx context.Context, value role.Config, changedBy string) (role.Config, error) {
|
|
_ = changedBy
|
|
value = role.NormalizeConfig(value)
|
|
if err := value.Validate(); err != nil {
|
|
return role.Config{}, err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return role.Config{}, fmt.Errorf("begin upsert role config: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
result, err := tx.ExecContext(ctx, `
|
|
UPDATE roles
|
|
SET config_toml = ?, auth_json = ?, updated_at = ?
|
|
WHERE name = ?
|
|
`, value.ConfigTOML, value.AuthJSON, s.now(), value.RoleName)
|
|
if err != nil {
|
|
return role.Config{}, fmt.Errorf("upsert role config: %w", err)
|
|
}
|
|
if err := ensureAffected(result, sql.ErrNoRows); err != nil {
|
|
return role.Config{}, err
|
|
}
|
|
after, err := getRoleConfigTx(ctx, tx, value.RoleName)
|
|
if err != nil {
|
|
return role.Config{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return role.Config{}, fmt.Errorf("commit upsert role config: %w", err)
|
|
}
|
|
return after, nil
|
|
}
|
|
|
|
func (s *Store) GetRoleConfig(ctx context.Context, roleName string) (role.Config, error) {
|
|
row := s.db.QueryRowContext(ctx, `
|
|
SELECT name, config_toml, auth_json
|
|
FROM roles
|
|
WHERE name = ?
|
|
`, roleName)
|
|
return scanRoleConfig(row)
|
|
}
|
|
|
|
func (s *Store) UpsertRoleSkillBinding(ctx context.Context, value role.SkillBinding, changedBy string) (role.SkillBinding, error) {
|
|
if err := value.Validate(); err != nil {
|
|
return role.SkillBinding{}, err
|
|
}
|
|
return upsertVersionedConfig(ctx, s, value, changedBy, versionedConfigUpsertSpec[role.SkillBinding]{
|
|
entityName: "role skill binding",
|
|
idKind: "role-skill-binding",
|
|
loadTx: func(ctx context.Context, tx *sql.Tx) (role.SkillBinding, error) {
|
|
return getRoleSkillBindingTx(ctx, tx, value.RoleName, value.WorkspaceID, value.SkillID)
|
|
},
|
|
current: func(item role.SkillBinding) versionedConfigCurrent {
|
|
return versionedConfigCurrent{
|
|
ID: item.ID,
|
|
Version: item.Version,
|
|
CreatedAt: item.CreatedAt,
|
|
}
|
|
},
|
|
applyMetadata: func(item *role.SkillBinding, meta versionedConfigMetadata) {
|
|
item.ID = meta.ID
|
|
item.Version = meta.Version
|
|
item.CreatedAt = meta.CreatedAt
|
|
item.UpdatedAt = meta.UpdatedAt
|
|
},
|
|
writeTx: func(ctx context.Context, tx *sql.Tx, item role.SkillBinding) error {
|
|
configJSON, err := marshalJSONMapAny(item.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO role_skill_bindings(id, role_name, workspace_id, skill_id, is_enabled, sort_order, config_json, version, updated_by, created_at, updated_at)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
is_enabled = excluded.is_enabled,
|
|
sort_order = excluded.sort_order,
|
|
config_json = excluded.config_json,
|
|
version = excluded.version,
|
|
updated_by = excluded.updated_by,
|
|
updated_at = excluded.updated_at
|
|
`,
|
|
item.ID,
|
|
item.RoleName,
|
|
nullableString(item.WorkspaceID),
|
|
item.SkillID,
|
|
boolToInt(item.IsEnabled),
|
|
item.SortOrder,
|
|
configJSON,
|
|
item.Version,
|
|
coalesceString(changedBy, item.UpdatedBy),
|
|
item.CreatedAt,
|
|
item.UpdatedAt,
|
|
)
|
|
return err
|
|
},
|
|
})
|
|
}
|
|
|
|
func (s *Store) ListRoleSkillBindings(ctx context.Context, roleName string) ([]role.SkillBinding, error) {
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT id, role_name, workspace_id, skill_id, is_enabled, sort_order, config_json, version, updated_by, created_at, updated_at
|
|
FROM role_skill_bindings
|
|
WHERE role_name = ?
|
|
ORDER BY sort_order, skill_id, workspace_id
|
|
`, roleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list role skill bindings: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []role.SkillBinding
|
|
for rows.Next() {
|
|
item, err := scanRoleSkillBinding(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate role skill bindings: %w", err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func getRoleTx(ctx context.Context, tx *sql.Tx, name string) (role.Definition, error) {
|
|
row := tx.QueryRowContext(ctx, `
|
|
SELECT name, title, executor_kind, description, is_enabled, is_builtin, sort_order, created_at, updated_at
|
|
FROM roles
|
|
WHERE name = ?
|
|
`, name)
|
|
return scanRole(row)
|
|
}
|
|
|
|
func getRolePromptTx(ctx context.Context, tx *sql.Tx, roleName, workspaceID string, kind role.PromptKind) (role.Prompt, error) {
|
|
var row *sql.Row
|
|
if workspaceID == "" {
|
|
row = tx.QueryRowContext(ctx, `
|
|
SELECT id, role_name, workspace_id, prompt_kind, content_markdown, version, updated_by, created_at, updated_at
|
|
FROM role_prompts
|
|
WHERE role_name = ? AND prompt_kind = ? AND workspace_id IS NULL
|
|
`, roleName, string(kind))
|
|
} else {
|
|
row = tx.QueryRowContext(ctx, `
|
|
SELECT id, role_name, workspace_id, prompt_kind, content_markdown, version, updated_by, created_at, updated_at
|
|
FROM role_prompts
|
|
WHERE role_name = ? AND prompt_kind = ? AND workspace_id = ?
|
|
`, roleName, string(kind), workspaceID)
|
|
}
|
|
return scanRolePrompt(row)
|
|
}
|
|
|
|
func getRoleConfigTx(ctx context.Context, tx *sql.Tx, roleName string) (role.Config, error) {
|
|
row := tx.QueryRowContext(ctx, `
|
|
SELECT name, config_toml, auth_json
|
|
FROM roles
|
|
WHERE name = ?
|
|
`, roleName)
|
|
return scanRoleConfig(row)
|
|
}
|
|
|
|
func getRoleSkillBindingTx(ctx context.Context, tx *sql.Tx, roleName, workspaceID, skillID string) (role.SkillBinding, error) {
|
|
var row *sql.Row
|
|
if workspaceID == "" {
|
|
row = tx.QueryRowContext(ctx, `
|
|
SELECT id, role_name, workspace_id, skill_id, is_enabled, sort_order, config_json, version, updated_by, created_at, updated_at
|
|
FROM role_skill_bindings
|
|
WHERE role_name = ? AND skill_id = ? AND workspace_id IS NULL
|
|
`, roleName, skillID)
|
|
} else {
|
|
row = tx.QueryRowContext(ctx, `
|
|
SELECT id, role_name, workspace_id, skill_id, is_enabled, sort_order, config_json, version, updated_by, created_at, updated_at
|
|
FROM role_skill_bindings
|
|
WHERE role_name = ? AND skill_id = ? AND workspace_id = ?
|
|
`, roleName, skillID, workspaceID)
|
|
}
|
|
return scanRoleSkillBinding(row)
|
|
}
|
|
|
|
func scanRole(s scanner) (role.Definition, error) {
|
|
var item role.Definition
|
|
var executorKind string
|
|
if err := s.Scan(&item.Name, &item.Title, &executorKind, &item.Description, &item.IsEnabled, &item.IsBuiltin, &item.SortOrder, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return role.Definition{}, err
|
|
}
|
|
item.ExecutorKind = role.ExecutorKind(executorKind)
|
|
item = role.NormalizeDefinition(item)
|
|
return item, nil
|
|
}
|
|
|
|
func scanRolePrompt(s scanner) (role.Prompt, error) {
|
|
var item role.Prompt
|
|
var workspaceID sql.NullString
|
|
var kind string
|
|
if err := s.Scan(&item.ID, &item.RoleName, &workspaceID, &kind, &item.ContentMarkdown, &item.Version, &item.UpdatedBy, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return role.Prompt{}, err
|
|
}
|
|
if workspaceID.Valid {
|
|
item.WorkspaceID = workspaceID.String
|
|
}
|
|
item.PromptKind = role.PromptKind(kind)
|
|
return item, nil
|
|
}
|
|
|
|
func scanRoleConfig(s scanner) (role.Config, error) {
|
|
var item role.Config
|
|
if err := s.Scan(&item.RoleName, &item.ConfigTOML, &item.AuthJSON); err != nil {
|
|
return role.Config{}, err
|
|
}
|
|
return role.NormalizeConfig(item), nil
|
|
}
|
|
|
|
func scanRoleSkillBinding(s scanner) (role.SkillBinding, error) {
|
|
var item role.SkillBinding
|
|
var workspaceID sql.NullString
|
|
var configJSON string
|
|
if err := s.Scan(&item.ID, &item.RoleName, &workspaceID, &item.SkillID, &item.IsEnabled, &item.SortOrder, &configJSON, &item.Version, &item.UpdatedBy, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
|
return role.SkillBinding{}, err
|
|
}
|
|
if workspaceID.Valid {
|
|
item.WorkspaceID = workspaceID.String
|
|
}
|
|
if err := unmarshalJSON(configJSON, &item.Config); err != nil {
|
|
return role.SkillBinding{}, fmt.Errorf("decode role skill binding config: %w", err)
|
|
}
|
|
return item, nil
|
|
}
|