File size: 3,125 Bytes
6f91ce5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// Export a board for the demo fixtures (server/demoBoards): every record a visitor needs, plus the
// board row, from a saved DATA_DIR (e.g. a live run kept with LIVE_KEEP_DATA=1).
// Usage: node server/exportBoard.ts <dataDir> <boardId> <out.json>
import { existsSync } from 'node:fs'
import { writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { boardDb, closeBoardDb, findBoard, setBoardDbPath } from './boardDb.ts'
import { safeId } from './rooms.ts'

const [dataDir, boardId, out] = process.argv.slice(2)
if (!dataDir || !boardId || !out) {
	console.error('Usage: node server/exportBoard.ts <dataDir> <boardId> <out.json>')
	process.exit(1)
}

// the room's tldraw records, straight from its SQLite file like the live check's room.py reads them
const roomFile = join(resolve(dataDir), 'rooms', `${safeId(boardId)}.db`)
if (!existsSync(roomFile)) {
	console.error(`no room file at ${roomFile}`)
	process.exit(1)
}
const room = new DatabaseSync(roomFile, { readOnly: true })
const records = room
	.prepare('SELECT state FROM documents')
	.all()
	.map((r) => {
		const state = (r as { state: string | Uint8Array }).state
		return JSON.parse(typeof state === 'string' ? state : new TextDecoder().decode(state)) as { typeName?: string }
	})
	// putShapes writes shapes and bindings; pages and assets go straight into the room
	.filter((r) => r.typeName === 'shape' || r.typeName === 'binding' || r.typeName === 'asset' || r.typeName === 'page')
room.close()

setBoardDbPath(join(resolve(dataDir), 'board.db'))
const board = findBoard(boardId)
if (!board) {
	console.error(`no board row for ${boardId} in ${dataDir}/board.db`)
	process.exit(1)
}
const rows = (sql: string) => boardDb().prepare(sql).all(safeId(boardId)) as Record<string, unknown>[]

const exported = {
	title: board.title,
	question: board.question,
	context: board.context,
	goals: board.goals,
	records,
	// per-board state the widgets read back: votes (card faces carry keepBy/discardBy too, but the
	// REST answers come from decisions) and links people removed so the agent never redraws them
	decisions: rows('SELECT paper_id, member_id, decision FROM decisions WHERE board_id = ? ORDER BY at, rowid').map(
		(r) => ({ paperId: String(r.paper_id), memberId: String(r.member_id), decision: String(r.decision) })
	),
	// the people who voted or wrote notes, so their names show on the demo board
	members: rows('SELECT member_id, display_name FROM members WHERE board_id = ? ORDER BY joined_at, rowid').map(
		(r) => ({ memberId: String(r.member_id), displayName: String(r.display_name) })
	),
	removedLinks: rows(
		'SELECT from_paper, to_paper, kind, removed_by FROM removed_links WHERE board_id = ? ORDER BY at, rowid'
	).map((r) => ({ from: String(r.from_paper), to: String(r.to_paper), kind: String(r.kind), removedBy: String(r.removed_by) })),
}
closeBoardDb()
writeFileSync(out, JSON.stringify(exported, null, '\t') + '\n')
console.log(`${boardId}: ${records.length} records, ${exported.decisions.length} decisions, ${exported.removedLinks.length} removed links → ${out}`)