Add title, priority, tags to cache; add schema migration
This commit is contained in:
		
							parent
							
								
									1b8ebab5f3
								
							
						
					
					
						commit
						d4330e86ac
					
				
					 3 changed files with 127 additions and 19 deletions
				
			
		|  | @ -3,32 +3,61 @@ package server | ||||||
| import ( | import ( | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	_ "github.com/mattn/go-sqlite3" // SQLite driver | 	_ "github.com/mattn/go-sqlite3" // SQLite driver | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Messages cache | ||||||
| const ( | const ( | ||||||
| 	createTableQuery = ` | 	createMessagesTableQuery = ` | ||||||
| 		BEGIN; | 		BEGIN; | ||||||
| 		CREATE TABLE IF NOT EXISTS messages ( | 		CREATE TABLE IF NOT EXISTS messages ( | ||||||
| 			id VARCHAR(20) PRIMARY KEY, | 			id VARCHAR(20) PRIMARY KEY, | ||||||
| 			time INT NOT NULL, | 			time INT NOT NULL, | ||||||
| 			topic VARCHAR(64) NOT NULL, | 			topic VARCHAR(64) NOT NULL, | ||||||
| 			message VARCHAR(1024) NOT NULL | 			message VARCHAR(512) NOT NULL, | ||||||
|  | 			title VARCHAR(256) NOT NULL, | ||||||
|  | 			priority INT NOT NULL, | ||||||
|  | 			tags VARCHAR(256) NOT NULL | ||||||
| 		); | 		); | ||||||
| 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); | 		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic); | ||||||
| 		COMMIT; | 		COMMIT; | ||||||
| 	` | 	` | ||||||
| 	insertMessageQuery           = `INSERT INTO messages (id, time, topic, message) VALUES (?, ?, ?, ?)` | 	insertMessageQuery           = `INSERT INTO messages (id, time, topic, message, title, priority, tags) VALUES (?, ?, ?, ?, ?, ?, ?)` | ||||||
| 	pruneMessagesQuery           = `DELETE FROM messages WHERE time < ?` | 	pruneMessagesQuery           = `DELETE FROM messages WHERE time < ?` | ||||||
| 	selectMessagesSinceTimeQuery = ` | 	selectMessagesSinceTimeQuery = ` | ||||||
| 		SELECT id, time, message  | 		SELECT id, time, message, title, priority, tags | ||||||
| 		FROM messages  | 		FROM messages  | ||||||
| 		WHERE topic = ? AND time >= ? | 		WHERE topic = ? AND time >= ? | ||||||
| 		ORDER BY time ASC | 		ORDER BY time ASC | ||||||
| 	` | 	` | ||||||
| 	selectMessageCountQuery = `SELECT count(*) FROM messages WHERE topic = ?` | 	selectMessagesCountQuery        = `SELECT COUNT(*) FROM messages` | ||||||
| 	selectTopicsQuery       = `SELECT topic, MAX(time) FROM messages GROUP BY TOPIC` | 	selectMessageCountForTopicQuery = `SELECT count(*) FROM messages WHERE topic = ?` | ||||||
|  | 	selectTopicsQuery               = `SELECT topic, MAX(time) FROM messages GROUP BY topic` | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Schema management queries | ||||||
|  | const ( | ||||||
|  | 	currentSchemaVersion          = 1 | ||||||
|  | 	createSchemaVersionTableQuery = ` | ||||||
|  | 		CREATE TABLE IF NOT EXISTS schemaVersion ( | ||||||
|  | 			id INT PRIMARY KEY, | ||||||
|  | 			version INT NOT NULL | ||||||
|  | 		); | ||||||
|  | 	` | ||||||
|  | 	insertSchemaVersion      = `INSERT INTO schemaVersion VALUES (1, ?)` | ||||||
|  | 	selectSchemaVersionQuery = `SELECT version FROM schemaVersion WHERE id = 1` | ||||||
|  | 
 | ||||||
|  | 	// 0 -> 1 | ||||||
|  | 	migrate0To1AlterMessagesTableQuery = ` | ||||||
|  | 		BEGIN; | ||||||
|  | 		ALTER TABLE messages ADD COLUMN title VARCHAR(256) NOT NULL DEFAULT(''); | ||||||
|  | 		ALTER TABLE messages ADD COLUMN priority INT NOT NULL DEFAULT(0); | ||||||
|  | 		ALTER TABLE messages ADD COLUMN tags VARCHAR(256) NOT NULL DEFAULT(''); | ||||||
|  | 		COMMIT; | ||||||
|  | 	` | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type sqliteCache struct { | type sqliteCache struct { | ||||||
|  | @ -42,7 +71,7 @@ func newSqliteCache(filename string) (*sqliteCache, error) { | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if _, err := db.Exec(createTableQuery); err != nil { | 	if err := setupDB(db); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return &sqliteCache{ | 	return &sqliteCache{ | ||||||
|  | @ -51,7 +80,7 @@ func newSqliteCache(filename string) (*sqliteCache, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *sqliteCache) AddMessage(m *message) error { | func (c *sqliteCache) AddMessage(m *message) error { | ||||||
| 	_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message) | 	_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ",")) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -64,19 +93,27 @@ func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error | ||||||
| 	messages := make([]*message, 0) | 	messages := make([]*message, 0) | ||||||
| 	for rows.Next() { | 	for rows.Next() { | ||||||
| 		var timestamp int64 | 		var timestamp int64 | ||||||
| 		var id, msg string | 		var priority int | ||||||
| 		if err := rows.Scan(&id, ×tamp, &msg); err != nil { | 		var id, msg, title, tagsStr string | ||||||
|  | 		if err := rows.Scan(&id, ×tamp, &msg, &title, &priority, &tagsStr); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		if msg == "" { | 		if msg == "" { | ||||||
| 			msg = " " // Hack: never return empty messages; this should not happen | 			msg = " " // Hack: never return empty messages; this should not happen | ||||||
| 		} | 		} | ||||||
|  | 		var tags []string | ||||||
|  | 		if tagsStr != "" { | ||||||
|  | 			tags = strings.Split(tagsStr, ",") | ||||||
|  | 		} | ||||||
| 		messages = append(messages, &message{ | 		messages = append(messages, &message{ | ||||||
| 			ID:      id, | 			ID:       id, | ||||||
| 			Time:    timestamp, | 			Time:     timestamp, | ||||||
| 			Event:   messageEvent, | 			Event:    messageEvent, | ||||||
| 			Topic:   topic, | 			Topic:    topic, | ||||||
| 			Message: msg, | 			Message:  msg, | ||||||
|  | 			Title:    title, | ||||||
|  | 			Priority: priority, | ||||||
|  | 			Tags:     tags, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| 	if err := rows.Err(); err != nil { | 	if err := rows.Err(); err != nil { | ||||||
|  | @ -86,7 +123,7 @@ func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *sqliteCache) MessageCount(topic string) (int, error) { | func (c *sqliteCache) MessageCount(topic string) (int, error) { | ||||||
| 	rows, err := c.db.Query(selectMessageCountQuery, topic) | 	rows, err := c.db.Query(selectMessageCountForTopicQuery, topic) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return 0, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
|  | @ -124,7 +161,63 @@ func (s *sqliteCache) Topics() (map[string]*topic, error) { | ||||||
| 	return topics, nil | 	return topics, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c *sqliteCache) Prune(keep time.Duration) error { | func (s *sqliteCache) Prune(keep time.Duration) error { | ||||||
| 	_, err := c.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix()) | 	_, err := s.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix()) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func setupDB(db *sql.DB) error { | ||||||
|  | 	// If 'messages' table does not exist, this must be a new database | ||||||
|  | 	rowsMC, err := db.Query(selectMessagesCountQuery) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return setupNewDB(db) | ||||||
|  | 	} | ||||||
|  | 	defer rowsMC.Close() | ||||||
|  | 
 | ||||||
|  | 	// If 'messages' table exists, check 'schemaVersion' table | ||||||
|  | 	schemaVersion := 0 | ||||||
|  | 	rowsSV, err := db.Query(selectSchemaVersionQuery) | ||||||
|  | 	if err == nil { | ||||||
|  | 		defer rowsSV.Close() | ||||||
|  | 		if !rowsSV.Next() { | ||||||
|  | 			return errors.New("cannot determine schema version: cache file may be corrupt") | ||||||
|  | 		} | ||||||
|  | 		if err := rowsSV.Scan(&schemaVersion); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Do migrations | ||||||
|  | 	if schemaVersion == currentSchemaVersion { | ||||||
|  | 		return nil | ||||||
|  | 	} else if schemaVersion == 0 { | ||||||
|  | 		return migrateFrom0To1(db) | ||||||
|  | 	} | ||||||
|  | 	return fmt.Errorf("unexpected schema version found: %d", schemaVersion) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func setupNewDB(db *sql.DB) error { | ||||||
|  | 	if _, err := db.Exec(createMessagesTableQuery); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := db.Exec(createSchemaVersionTableQuery); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := db.Exec(insertSchemaVersion, currentSchemaVersion); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func migrateFrom0To1(db *sql.DB) error { | ||||||
|  | 	if _, err := db.Exec(migrate0To1AlterMessagesTableQuery); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := db.Exec(createSchemaVersionTableQuery); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if _, err := db.Exec(insertSchemaVersion, 1); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -382,6 +382,11 @@ li { | ||||||
|     font-size: 0.9em; |     font-size: 0.9em; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | #detail .detailTitle { | ||||||
|  |     font-weight: bold; | ||||||
|  |     font-size: 1.1em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| #detail .detailMessage { | #detail .detailMessage { | ||||||
|     margin-bottom: 20px; |     margin-bottom: 20px; | ||||||
|     font-size: 1.1em; |     font-size: 1.1em; | ||||||
|  |  | ||||||
|  | @ -86,10 +86,14 @@ const subscribeInternal = (topic, persist, delaySec) => { | ||||||
|             } |             } | ||||||
|             if (Notification.permission === "granted") { |             if (Notification.permission === "granted") { | ||||||
|                 notifySound.play(); |                 notifySound.play(); | ||||||
|                 new Notification(`${location.host}/${topic}`, { |                 const title = (event.title) ? event.title : `${location.host}/${topic}`; | ||||||
|  |                 const notification = new Notification(title, { | ||||||
|                     body: event.message, |                     body: event.message, | ||||||
|                     icon: '/static/img/favicon.png' |                     icon: '/static/img/favicon.png' | ||||||
|                 }); |                 }); | ||||||
|  |                 notification.onclick = (e) => { | ||||||
|  |                     showDetail(event.topic); | ||||||
|  |                 }; | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         topics[topic] = { |         topics[topic] = { | ||||||
|  | @ -149,6 +153,7 @@ const rerenderDetailView = () => { | ||||||
|     } |     } | ||||||
|     topics[currentTopic]['messages'].forEach(m => { |     topics[currentTopic]['messages'].forEach(m => { | ||||||
|         let dateDiv = document.createElement('div'); |         let dateDiv = document.createElement('div'); | ||||||
|  |         let titleDiv = document.createElement('div'); | ||||||
|         let messageDiv = document.createElement('div'); |         let messageDiv = document.createElement('div'); | ||||||
|         let eventDiv = document.createElement('div'); |         let eventDiv = document.createElement('div'); | ||||||
|         dateDiv.classList.add('detailDate'); |         dateDiv.classList.add('detailDate'); | ||||||
|  | @ -156,6 +161,11 @@ const rerenderDetailView = () => { | ||||||
|         messageDiv.classList.add('detailMessage'); |         messageDiv.classList.add('detailMessage'); | ||||||
|         messageDiv.innerText = m.message; |         messageDiv.innerText = m.message; | ||||||
|         eventDiv.appendChild(dateDiv); |         eventDiv.appendChild(dateDiv); | ||||||
|  |         if (m.title) { | ||||||
|  |             titleDiv.classList.add('detailTitle'); | ||||||
|  |             titleDiv.innerText = m.title; | ||||||
|  |             eventDiv.appendChild(titleDiv) | ||||||
|  |         } | ||||||
|         eventDiv.appendChild(messageDiv); |         eventDiv.appendChild(messageDiv); | ||||||
|         detailEventsList.appendChild(eventDiv); |         detailEventsList.appendChild(eventDiv); | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue