/** * Populate the dashboard SQLite database with fake usage data against * the first account in the DB so the Usage tab isn't empty. * * Run with: `npm run seed` (which uses node --env-file=.env). * * If there's no account yet, we create an anonymous one and also mint * two keys so the dashboard has something to show when you first log in. */ import Database from 'better-sqlite3'; import { mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { createHash, randomBytes, randomUUID } from 'node:crypto'; const DB_PATH = resolve(process.env.DATABASE_PATH || './data/dashboard.db'); mkdirSync(dirname(DB_PATH), { recursive: true }); const db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); // Make sure the schema exists (same as src/lib/server/db.ts). db.exec(` CREATE TABLE IF NOT EXISTS accounts ( id TEXT PRIMARY KEY, email TEXT UNIQUE, plan TEXT NOT NULL DEFAULT 'free', created_at INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS api_keys ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, key_hash TEXT NOT NULL UNIQUE, key_prefix TEXT NOT NULL, name TEXT NOT NULL, scopes TEXT NOT NULL DEFAULT '["*"]', active INTEGER NOT NULL DEFAULT 1, created_at INTEGER NOT NULL, last_used_at INTEGER ); CREATE INDEX IF NOT EXISTS idx_api_keys_account ON api_keys(account_id); CREATE TABLE IF NOT EXISTS magic_links ( id TEXT PRIMARY KEY, account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, expires_at INTEGER NOT NULL, used INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_magic_links_token ON magic_links(token); CREATE TABLE IF NOT EXISTS usage_events ( id TEXT PRIMARY KEY, api_key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, dataset TEXT NOT NULL, timestamp INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_usage_key ON usage_events(api_key_id); CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp); CREATE INDEX IF NOT EXISTS idx_usage_dataset ON usage_events(dataset); `); const now = Date.now(); const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; function mintKey() { const bytes = randomBytes(32); let random = ''; for (let i = 0; i < 32; i++) { random += ALPHABET[bytes[i] % ALPHABET.length]; } const key = `ti_${random}`; const hash = createHash('sha256').update(key).digest('hex'); const prefix = key.slice(0, 8); return { key, hash, prefix }; } // --- find or create an account --- /** @typedef {{ id: string, plan: string }} AccountRow */ /** @typedef {{ id: string, name: string }} KeyRow */ let account = /** @type {AccountRow | undefined} */ ( db .prepare(`SELECT id, plan FROM accounts ORDER BY created_at ASC LIMIT 1`) .get() ); if (!account) { const id = randomUUID(); db.prepare( `INSERT INTO accounts (id, email, plan, created_at) VALUES (?, NULL, 'dev', ?)` ).run(id, now - 1000 * 60 * 60 * 24 * 14); // created 2 weeks ago account = { id, plan: 'dev' }; console.log(`created demo account ${id}`); } else { console.log(`seeding against existing account ${account.id}`); } // --- make sure there are at least two active keys --- const existingKeys = /** @type {KeyRow[]} */ ( db .prepare(`SELECT id, name FROM api_keys WHERE account_id = ? AND active = 1`) .all(account.id) ); /** @type {KeyRow[]} */ const keyRows = existingKeys.length >= 2 ? existingKeys : (() => { /** @type {KeyRow[]} */ const created = []; for (const name of ['production ingest', 'research notebook']) { const { hash, prefix } = mintKey(); const id = randomUUID(); db.prepare( `INSERT INTO api_keys (id, account_id, key_hash, key_prefix, name, scopes, active, created_at) VALUES (?, ?, ?, ?, ?, '["*"]', 1, ?)` ).run(id, account.id, hash, prefix, name, now - 1000 * 60 * 60 * 24 * 10); created.push({ id, name }); } return [...existingKeys, ...created]; })(); // --- wipe usage events we seeded previously and re-generate --- db.prepare( `DELETE FROM usage_events WHERE api_key_id IN (SELECT id FROM api_keys WHERE account_id = ?)` ).run(account.id); // Generate roughly 2,400 events distributed over the past 30 days, heavily // weighted toward irs-990 and sec-edgar (the flagship datasets). const weights = { 'irs-990': 28, 'sec-edgar': 22, 'sec-13f': 14, 'fec-contributions': 10, 'nih-reporter': 8, 'pacer': 6, 'fda-faers': 4, 'osha': 3, 'usaspending': 3, 'cfpb-complaints': 2 }; const weighted = /** @type {string[]} */ ([]); for (const [slug, w] of Object.entries(weights)) { for (let i = 0; i < w; i++) weighted.push(slug); } const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; const insertEvent = db.prepare( `INSERT INTO usage_events (id, api_key_id, dataset, timestamp) VALUES (?, ?, ?, ?)` ); const TOTAL = 2400; const tx = db.transaction(() => { for (let i = 0; i < TOTAL; i++) { const dataset = weighted[Math.floor(Math.random() * weighted.length)]; const keyRow = keyRows[Math.floor(Math.random() * keyRows.length)]; const age = Math.floor(Math.random() * THIRTY_DAYS); insertEvent.run(randomUUID(), keyRow.id, dataset, now - age); } }); tx(); // Update last_used_at on each key to the most recent event timestamp. for (const row of keyRows) { const latest = /** @type {{ ts: number | null } | undefined} */ ( db .prepare( `SELECT MAX(timestamp) AS ts FROM usage_events WHERE api_key_id = ?` ) .get(row.id) ); if (latest?.ts) { db.prepare(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`).run( latest.ts, row.id ); } } console.log(`seeded ${TOTAL} usage events against ${keyRows.length} keys`); console.log(`db: ${DB_PATH}`); db.close();