Files

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
}