480 lines
12 KiB
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 ""
|
|
}
|
|
}
|