Introduces the web backend (auth, RBAC, project tokens, revisions), YouTrack-or-taskmaster backend choice in the TUI, and a first-pass embedded Svelte UI. The frontend is functional but needs a major rework. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Revision is a versioned snapshot of an entity.
|
|
type Revision struct {
|
|
ID int64 `json:"id"`
|
|
EntityType string `json:"entityType"`
|
|
EntityID string `json:"entityId"`
|
|
ProjectID string `json:"projectId"`
|
|
Rev int `json:"rev"`
|
|
At time.Time `json:"at"`
|
|
ActorUserID string `json:"actorUserId,omitempty"`
|
|
Op string `json:"op"`
|
|
Snapshot json.RawMessage `json:"snapshot"`
|
|
}
|
|
|
|
// AppendRevision stores a new revision snapshot.
|
|
func (db *DB) AppendRevision(entityType, entityID, projectID, op, actorUserID string, snapshot any) (Revision, error) {
|
|
data, err := json.Marshal(snapshot)
|
|
if err != nil {
|
|
return Revision{}, err
|
|
}
|
|
var maxRev sql.NullInt64
|
|
_ = db.SQL.QueryRow(`SELECT MAX(rev) FROM revisions WHERE entity_type = ? AND entity_id = ?`,
|
|
entityType, entityID).Scan(&maxRev)
|
|
rev := 1
|
|
if maxRev.Valid {
|
|
rev = int(maxRev.Int64) + 1
|
|
}
|
|
at := time.Now().UTC()
|
|
var actor any
|
|
if actorUserID != "" {
|
|
actor = actorUserID
|
|
}
|
|
res, err := db.SQL.Exec(`
|
|
INSERT INTO revisions(entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json)
|
|
VALUES(?,?,?,?,?,?,?,?)`,
|
|
entityType, entityID, projectID, rev, formatTime(at), actor, op, string(data))
|
|
if err != nil {
|
|
return Revision{}, err
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
return Revision{
|
|
ID: id,
|
|
EntityType: entityType,
|
|
EntityID: entityID,
|
|
ProjectID: projectID,
|
|
Rev: rev,
|
|
At: at,
|
|
ActorUserID: actorUserID,
|
|
Op: op,
|
|
Snapshot: data,
|
|
}, nil
|
|
}
|
|
|
|
// ListRevisions returns revisions for an entity, newest first.
|
|
func (db *DB) ListRevisions(entityType, entityID string) ([]Revision, error) {
|
|
rows, err := db.SQL.Query(`
|
|
SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json
|
|
FROM revisions WHERE entity_type = ? AND entity_id = ?
|
|
ORDER BY rev DESC`, entityType, entityID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanRevisions(rows)
|
|
}
|
|
|
|
// GetRevision returns one revision by id.
|
|
func (db *DB) GetRevision(id int64) (Revision, error) {
|
|
row := db.SQL.QueryRow(`
|
|
SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json
|
|
FROM revisions WHERE id = ?`, id)
|
|
var r Revision
|
|
var at string
|
|
var actor sql.NullString
|
|
var snap string
|
|
err := row.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap)
|
|
if err == sql.ErrNoRows {
|
|
return Revision{}, fmt.Errorf("revision %d: not found", id)
|
|
}
|
|
if err != nil {
|
|
return Revision{}, err
|
|
}
|
|
r.At = parseTime(at)
|
|
if actor.Valid {
|
|
r.ActorUserID = actor.String
|
|
}
|
|
r.Snapshot = json.RawMessage(snap)
|
|
return r, nil
|
|
}
|
|
|
|
func scanRevisions(rows *sql.Rows) ([]Revision, error) {
|
|
var out []Revision
|
|
for rows.Next() {
|
|
var r Revision
|
|
var at string
|
|
var actor sql.NullString
|
|
var snap string
|
|
if err := rows.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap); err != nil {
|
|
return nil, err
|
|
}
|
|
r.At = parseTime(at)
|
|
if actor.Valid {
|
|
r.ActorUserID = actor.String
|
|
}
|
|
r.Snapshot = json.RawMessage(snap)
|
|
out = append(out, r)
|
|
}
|
|
if out == nil {
|
|
out = []Revision{}
|
|
}
|
|
return out, rows.Err()
|
|
}
|