package store import ( "context" "database/sql" "encoding/json" "errors" "time" "housecarl/internal/model" ) const changesetCols = `id, user_id, username, via, description, status, ops, created_at, updated_at, history_id` func scanChangeset(sc interface{ Scan(...any) error }) (*model.Changeset, error) { cs := &model.Changeset{} var uid, hid sql.NullInt64 var ops, created, updated string if err := sc.Scan(&cs.ID, &uid, &cs.Username, &cs.Via, &cs.Description, &cs.Status, &ops, &created, &updated, &hid); err != nil { return nil, err } cs.UserID, cs.HistoryID = uid.Int64, hid.Int64 cs.CreatedAt, cs.UpdatedAt = parseTS(created), parseTS(updated) if err := json.Unmarshal([]byte(ops), &cs.Ops); err != nil { return nil, err } if cs.Ops == nil { cs.Ops = []model.Op{} } return cs, nil } // CreateChangeset saves a new changeset and sets its ID. func (s *Store) CreateChangeset(ctx context.Context, cs *model.Changeset) error { now := time.Now() cs.CreatedAt, cs.UpdatedAt = now, now if cs.Ops == nil { cs.Ops = []model.Op{} } ops, _ := json.Marshal(cs.Ops) res, err := s.db.ExecContext(ctx, `INSERT INTO changesets (user_id, username, via, description, status, ops, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, nullID(cs.UserID), cs.Username, cs.Via, cs.Description, cs.Status, string(ops), ts(now), ts(now)) if err != nil { return err } cs.ID, err = res.LastInsertId() return err } // GetChangeset loads one changeset. func (s *Store) GetChangeset(ctx context.Context, id int64) (*model.Changeset, error) { cs, err := scanChangeset(s.db.QueryRowContext(ctx, `SELECT `+changesetCols+` FROM changesets WHERE id = ?`, id)) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return cs, err } // ListChangesets lists changesets, newest first: of a status ("" any), // and of a user (0 anyone's). func (s *Store) ListChangesets(ctx context.Context, status string, userID int64, limit int) ([]*model.Changeset, error) { if limit <= 0 || limit > 500 { limit = 100 } q := `SELECT ` + changesetCols + ` FROM changesets WHERE 1=1` var args []any if status != "" { q += ` AND status = ?` args = append(args, status) } if userID != 0 { q += ` AND user_id = ?` args = append(args, userID) } q += ` ORDER BY id DESC LIMIT ?` args = append(args, limit) rows, err := s.db.QueryContext(ctx, q, args...) if err != nil { return nil, err } defer rows.Close() out := []*model.Changeset{} for rows.Next() { cs, err := scanChangeset(rows) if err != nil { return nil, err } out = append(out, cs) } return out, rows.Err() } // UpdateChangeset saves a changeset's description, status, operations and // history entry. func (s *Store) UpdateChangeset(ctx context.Context, cs *model.Changeset) error { cs.UpdatedAt = time.Now() ops, _ := json.Marshal(cs.Ops) res, err := s.db.ExecContext(ctx, `UPDATE changesets SET description = ?, status = ?, ops = ?, updated_at = ?, history_id = ? WHERE id = ?`, cs.Description, cs.Status, string(ops), ts(cs.UpdatedAt), nullID(cs.HistoryID), cs.ID) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } func nullID(id int64) any { if id == 0 { return nil } return id }