feat: implement SQLite storage for state messages and add message retrieval endpoint
This commit is contained in:
@@ -3,9 +3,11 @@ package handler
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.piskot.si/SeminarM2/lambdaiot-core/internal/storage"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
@@ -243,3 +245,26 @@ func (h *Handler) Protected(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
|
||||
}
|
||||
|
||||
// GetMessages returns the last 100 state messages with optional pagination
|
||||
// query parameter: ?page=N (0-based)
|
||||
func GetMessages(c *gin.Context) {
|
||||
page := 0
|
||||
if p := c.Query("page"); p != "" {
|
||||
if n, err := strconv.Atoi(p); err == nil && n >= 0 {
|
||||
page = n
|
||||
}
|
||||
}
|
||||
limit := 100
|
||||
offset := page * limit
|
||||
if storage.Default == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "storage not initialized"})
|
||||
return
|
||||
}
|
||||
msgs, err := storage.Default.QueryMessages(limit, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"messages": msgs})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// DB wraps sql.DB
|
||||
type DB struct {
|
||||
conn *sql.DB
|
||||
}
|
||||
|
||||
// Default is the package-level DB used by handlers
|
||||
var Default *DB
|
||||
|
||||
// Init opens the sqlite database at path and ensures the table exists
|
||||
func Init(path string) (*DB, error) {
|
||||
conn, err := sql.Open("sqlite3", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// set reasonable pragmas
|
||||
if _, err := conn.Exec("PRAGMA journal_mode=WAL;"); err != nil {
|
||||
// ignore
|
||||
}
|
||||
schema := `CREATE TABLE IF NOT EXISTS messages (
|
||||
message TEXT NOT NULL,
|
||||
timestamp TEXT NOT NULL
|
||||
);`
|
||||
if _, err := conn.Exec(schema); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &DB{conn: conn}, nil
|
||||
}
|
||||
|
||||
// SetDefault sets the package default DB
|
||||
func SetDefault(d *DB) {
|
||||
Default = d
|
||||
}
|
||||
|
||||
// Close closes the underlying connection
|
||||
func (d *DB) Close() error {
|
||||
if d == nil || d.conn == nil {
|
||||
return nil
|
||||
}
|
||||
return d.conn.Close()
|
||||
}
|
||||
|
||||
// InsertMessage inserts a message with timestamp into the DB
|
||||
func (d *DB) InsertMessage(msg string, ts time.Time) error {
|
||||
if d == nil || d.conn == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := d.conn.Exec("INSERT INTO messages(message, timestamp) VALUES(?, ?)", msg, ts.Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
// Message is the returned message shape
|
||||
type Message struct {
|
||||
Message string `json:"message"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// QueryMessages returns messages ordered by newest first
|
||||
func (d *DB) QueryMessages(limit, offset int) ([]Message, error) {
|
||||
if d == nil || d.conn == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := d.conn.Query("SELECT message, timestamp FROM messages ORDER BY rowid DESC LIMIT ? OFFSET ?", limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
res := []Message{}
|
||||
for rows.Next() {
|
||||
var m Message
|
||||
if err := rows.Scan(&m.Message, &m.Timestamp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res = append(res, m)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
Reference in New Issue
Block a user