Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions hub/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { dirname } from 'node:path'
import { MachineStore } from './machineStore'
import { MessageStore } from './messageStore'
import { PushStore } from './pushStore'
import { ScratchlistStore } from './scratchlistStore'
import { SessionStore } from './sessionStore'
import { UserStore } from './userStore'

export type {
StoredMachine,
StoredMessage,
StoredPushSubscription,
StoredScratchlistEntry,
StoredSession,
StoredUser,
VersionedUpdateResult
Expand All @@ -20,16 +22,18 @@ export type { CancelQueuedMessageResult, LookupQueuedMessageResult } from './mes
export { MachineStore } from './machineStore'
export { MessageStore } from './messageStore'
export { PushStore } from './pushStore'
export { ScratchlistStore } from './scratchlistStore'
export { SessionStore } from './sessionStore'
export { UserStore } from './userStore'

const SCHEMA_VERSION: number = 9
const SCHEMA_VERSION: number = 10
const REQUIRED_TABLES = [
'sessions',
'machines',
'messages',
'users',
'push_subscriptions'
'push_subscriptions',
'session_scratchlist'
] as const

export class Store {
Expand All @@ -42,6 +46,7 @@ export class Store {
readonly messages: MessageStore
readonly users: UserStore
readonly push: PushStore
readonly scratchlist: ScratchlistStore

/**
* Filesystem path of the underlying SQLite database, or ':memory:' for
Expand Down Expand Up @@ -92,6 +97,7 @@ export class Store {
this.messages = new MessageStore(this.db)
this.users = new UserStore(this.db)
this.push = new PushStore(this.db)
this.scratchlist = new ScratchlistStore(this.db)
}

close(): void {
Expand Down Expand Up @@ -123,6 +129,7 @@ export class Store {
6: () => this.migrateFromV6ToV7(),
7: () => this.migrateFromV7ToV8(),
8: () => this.migrateFromV8ToV9(),
9: () => this.migrateFromV9ToV10(),
})

if (currentVersion === 0) {
Expand Down Expand Up @@ -250,6 +257,18 @@ export class Store {
UNIQUE(namespace, endpoint)
);
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_namespace ON push_subscriptions(namespace);

CREATE TABLE IF NOT EXISTS session_scratchlist (
session_id TEXT NOT NULL,
entry_id TEXT NOT NULL,
text TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (session_id, entry_id),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_session_scratchlist_session_created
ON session_scratchlist(session_id, created_at DESC);
`)
}

Expand Down Expand Up @@ -425,6 +444,41 @@ export class Store {
`)
}

/**
* tiann/hapi#893 (scratchlist v2): introduce the per-session
* `session_scratchlist` typed table. Operator-decided schema choice
* over an opaque metadata blob - the eventual overseer-context use
* case wants `(sessionId, createdAt)` queryability without parsing
* JSON.
*
* Idempotent via `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT
* EXISTS`. Cascade-delete from `sessions(id)` handles delete-session
* cleanup. No data backfill: pre-v10 hubs never had this data; the
* web client's first-run migration (`hapi.scratchlist.v2.migrated.*`
* flag) pushes any existing `localStorage` entries up via the REST
* endpoint.
*
* Rollback: `DROP TABLE session_scratchlist; PRAGMA user_version = 9;`
* - the table is independent, so the drop is safe and loses only the
* v2 hub-side entries (web client retains its localStorage offline
* cache).
*/
private migrateFromV9ToV10(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS session_scratchlist (
session_id TEXT NOT NULL,
entry_id TEXT NOT NULL,
text TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (session_id, entry_id),
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_session_scratchlist_session_created
ON session_scratchlist(session_id, created_at DESC);
`)
}

private getSessionColumnNames(): Set<string> {
const rows = this.db.prepare('PRAGMA table_info(sessions)').all() as Array<{ name: string }>
return new Set(rows.map((row) => row.name))
Expand Down
Loading
Loading