3588AdminBackend/internal/storage/assets_repo.go

480 lines
12 KiB
Go

package storage
import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
)
type AssetRecord struct {
Name string
Description string
TemplateName string
BusinessName string
BodyJSON string
CreatedAt string
UpdatedAt string
}
type IntegrationServiceRecord struct {
Name string
ServiceType string
Description string
Enabled bool
BodyJSON string
CreatedAt string
UpdatedAt string
}
type VideoSourceRecord struct {
Name string
SourceType string
Area string
Description string
BodyJSON string
CreatedAt string
UpdatedAt string
}
type AssetsRepo struct {
db *sql.DB
}
func NewAssetsRepo(db *sql.DB) *AssetsRepo {
return &AssetsRepo{db: db}
}
func (r *AssetsRepo) SaveTemplate(name string, description string, bodyJSON string) error {
return r.saveAsset("templates", AssetRecord{
Name: name,
Description: description,
BodyJSON: bodyJSON,
})
}
func (r *AssetsRepo) SaveProfile(name string, templateName string, businessName string, description string, bodyJSON string) error {
return r.saveAsset("profiles", AssetRecord{
Name: name,
TemplateName: templateName,
BusinessName: businessName,
Description: description,
BodyJSON: bodyJSON,
})
}
func (r *AssetsRepo) SaveOverlay(name string, description string, bodyJSON string) error {
return r.saveAsset("overlays", AssetRecord{
Name: name,
Description: description,
BodyJSON: bodyJSON,
})
}
func (r *AssetsRepo) SaveIntegrationService(name string, serviceType string, description string, enabled bool, bodyJSON string) error {
if r == nil || r.db == nil {
return nil
}
now := time.Now().Format(time.RFC3339)
_, err := r.db.Exec(`
INSERT INTO integration_services(name, type, description, enabled, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM integration_services WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
type=excluded.type,
description=excluded.description,
enabled=excluded.enabled,
body_json=excluded.body_json,
updated_at=excluded.updated_at
`, name, serviceType, description, enabled, bodyJSON, name, now, now)
return err
}
func (r *AssetsRepo) SaveVideoSource(name string, sourceType string, area string, description string, bodyJSON string) error {
if r == nil || r.db == nil {
return nil
}
now := time.Now().Format(time.RFC3339)
_, err := r.db.Exec(`
INSERT INTO video_sources(name, source_type, area, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM video_sources WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
source_type=excluded.source_type,
area=excluded.area,
description=excluded.description,
body_json=excluded.body_json,
updated_at=excluded.updated_at
`, name, sourceType, area, description, bodyJSON, name, now, now)
return err
}
func (r *AssetsRepo) ListTemplates() ([]AssetRecord, error) {
return r.listAssets("templates")
}
func (r *AssetsRepo) ListProfiles() ([]AssetRecord, error) {
return r.listAssets("profiles")
}
func (r *AssetsRepo) ListOverlays() ([]AssetRecord, error) {
return r.listAssets("overlays")
}
func (r *AssetsRepo) ListIntegrationServices() ([]IntegrationServiceRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
rows, err := r.db.Query(`
SELECT name, type, description, enabled, body_json, created_at, updated_at
FROM integration_services
ORDER BY updated_at DESC, name ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []IntegrationServiceRecord
for rows.Next() {
var item IntegrationServiceRecord
if err := rows.Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (r *AssetsRepo) ListVideoSources() ([]VideoSourceRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
rows, err := r.db.Query(`
SELECT name, source_type, area, description, body_json, created_at, updated_at
FROM video_sources
ORDER BY updated_at DESC, name ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []VideoSourceRecord
for rows.Next() {
var item VideoSourceRecord
if err := rows.Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (r *AssetsRepo) GetTemplate(name string) (*AssetRecord, error) {
return r.getAsset("templates", name)
}
func (r *AssetsRepo) GetProfile(name string) (*AssetRecord, error) {
return r.getAsset("profiles", name)
}
func (r *AssetsRepo) GetOverlay(name string) (*AssetRecord, error) {
return r.getAsset("overlays", name)
}
func (r *AssetsRepo) GetIntegrationService(name string) (*IntegrationServiceRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
var item IntegrationServiceRecord
err := r.db.QueryRow(`
SELECT name, type, description, enabled, body_json, created_at, updated_at
FROM integration_services
WHERE name = ?
`, name).Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &item, nil
}
func (r *AssetsRepo) GetVideoSource(name string) (*VideoSourceRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
var item VideoSourceRecord
err := r.db.QueryRow(`
SELECT name, source_type, area, description, body_json, created_at, updated_at
FROM video_sources
WHERE name = ?
`, name).Scan(&item.Name, &item.SourceType, &item.Area, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &item, nil
}
func (r *AssetsRepo) DeleteTemplate(name string) error {
return r.deleteAsset("templates", name)
}
func (r *AssetsRepo) DeleteIntegrationService(name string) error {
if r == nil || r.db == nil {
return nil
}
_, err := r.db.Exec(`DELETE FROM integration_services WHERE name = ?`, name)
return err
}
func (r *AssetsRepo) DeleteVideoSource(name string) error {
if r == nil || r.db == nil {
return nil
}
_, err := r.db.Exec(`DELETE FROM video_sources WHERE name = ?`, name)
return err
}
func (r *AssetsRepo) RenameTemplate(oldName string, newName string, description string, bodyJSON string) error {
if r == nil || r.db == nil {
return nil
}
oldName = strings.TrimSpace(oldName)
newName = strings.TrimSpace(newName)
if oldName == "" || newName == "" {
return fmt.Errorf("template name is required")
}
if oldName == newName {
return r.SaveTemplate(newName, description, bodyJSON)
}
tx, err := r.db.Begin()
if err != nil {
return err
}
defer func() {
if tx != nil {
_ = tx.Rollback()
}
}()
var exists int
if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, oldName).Scan(&exists); err != nil {
return err
}
if exists == 0 {
return sql.ErrNoRows
}
if err := tx.QueryRow(`SELECT COUNT(1) FROM templates WHERE name = ?`, newName).Scan(&exists); err != nil {
return err
}
if exists > 0 {
return fmt.Errorf("template %q already exists", newName)
}
now := time.Now().Format(time.RFC3339)
if _, err := tx.Exec(`
INSERT INTO templates(name, description, body_json, created_at, updated_at)
SELECT ?, ?, ?, created_at, ?
FROM templates
WHERE name = ?
`, newName, description, bodyJSON, now, oldName); err != nil {
return err
}
rows, err := tx.Query(`
SELECT name, description, business_name, body_json
FROM profiles
WHERE primary_template_name = ? OR body_json LIKE ?
`, oldName, "%\"template\":\""+oldName+"\"%")
if err != nil {
return err
}
defer rows.Close()
type profileUpdate struct {
name string
description string
businessName string
bodyJSON string
}
updates := make([]profileUpdate, 0)
for rows.Next() {
var item profileUpdate
if err := rows.Scan(&item.name, &item.description, &item.businessName, &item.bodyJSON); err != nil {
return err
}
rewritten, changed, err := rewriteProfileTemplateRefs(item.bodyJSON, oldName, newName)
if err != nil {
return err
}
if changed {
item.bodyJSON = rewritten
}
updates = append(updates, item)
}
if err := rows.Err(); err != nil {
return err
}
for _, item := range updates {
if _, err := tx.Exec(`
UPDATE profiles
SET primary_template_name = ?, body_json = ?, updated_at = ?
WHERE name = ?
`, newName, item.bodyJSON, now, item.name); err != nil {
return err
}
}
if _, err := tx.Exec(`DELETE FROM templates WHERE name = ?`, oldName); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
tx = nil
return nil
}
func (r *AssetsRepo) saveAsset(table string, record AssetRecord) error {
if r == nil || r.db == nil {
return nil
}
now := time.Now().Format(time.RFC3339)
switch table {
case "templates", "overlays":
_, err := r.db.Exec(`
INSERT INTO `+table+`(name, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, COALESCE((SELECT created_at FROM `+table+` WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
description=excluded.description,
body_json=excluded.body_json,
updated_at=excluded.updated_at
`, record.Name, record.Description, record.BodyJSON, record.Name, now, now)
return err
case "profiles":
_, err := r.db.Exec(`
INSERT INTO profiles(name, primary_template_name, business_name, description, body_json, created_at, updated_at)
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM profiles WHERE name = ?), ?), ?)
ON CONFLICT(name) DO UPDATE SET
primary_template_name=excluded.primary_template_name,
business_name=excluded.business_name,
description=excluded.description,
body_json=excluded.body_json,
updated_at=excluded.updated_at
`, record.Name, record.TemplateName, record.BusinessName, record.Description, record.BodyJSON, record.Name, now, now)
return err
default:
return nil
}
}
func (r *AssetsRepo) listAssets(table string) ([]AssetRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
query := `
SELECT name, description, body_json, created_at, updated_at, '', ''
FROM ` + table + `
ORDER BY updated_at DESC, name ASC
`
if table == "profiles" {
query = `
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
FROM profiles
ORDER BY updated_at DESC, name ASC
`
}
rows, err := r.db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var out []AssetRecord
for rows.Next() {
var item AssetRecord
if err := rows.Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName); err != nil {
return nil, err
}
out = append(out, item)
}
return out, rows.Err()
}
func (r *AssetsRepo) getAsset(table string, name string) (*AssetRecord, error) {
if r == nil || r.db == nil {
return nil, nil
}
query := `
SELECT name, description, body_json, created_at, updated_at, '', ''
FROM ` + table + `
WHERE name = ?
`
if table == "profiles" {
query = `
SELECT name, description, body_json, created_at, updated_at, primary_template_name, business_name
FROM profiles
WHERE name = ?
`
}
var item AssetRecord
err := r.db.QueryRow(query, name).Scan(&item.Name, &item.Description, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt, &item.TemplateName, &item.BusinessName)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &item, nil
}
func (r *AssetsRepo) deleteAsset(table string, name string) error {
if r == nil || r.db == nil {
return nil
}
_, err := r.db.Exec(`DELETE FROM `+table+` WHERE name = ?`, name)
return err
}
func rewriteProfileTemplateRefs(bodyJSON string, oldName string, newName string) (string, bool, error) {
var raw map[string]any
if err := json.Unmarshal([]byte(bodyJSON), &raw); err != nil {
return "", false, err
}
changed := false
instances, _ := raw["instances"].([]any)
for _, item := range instances {
instanceMap, _ := item.(map[string]any)
if strings.TrimSpace(valueString(instanceMap["template"])) == oldName {
instanceMap["template"] = newName
changed = true
}
}
if !changed {
return bodyJSON, false, nil
}
body, err := json.MarshalIndent(raw, "", " ")
if err != nil {
return "", false, err
}
return string(append(body, '\n')), true, nil
}
func valueString(v any) string {
switch vv := v.(type) {
case string:
return vv
case float64:
return fmt.Sprintf("%v", vv)
default:
return ""
}
}