From 608fa7fbc9f0f225bef42e915c0dcb49d096ca94 Mon Sep 17 00:00:00 2001 From: Meme11030 Date: Tue, 2 Jun 2026 10:31:24 +0000 Subject: [PATCH] chore: drop better-sqlite3 and remove disciplr.db artifacts - Remove better-sqlite3 and @types/better-sqlite3 from package.json - Delete data/disciplr.db (SQLite database file) - Replace src/db/database.ts with PostgreSQL-only implementation - Add ESLint no-restricted-imports rule blocking better-sqlite3 - Update docs/analytics-storage.md to remove SQLite references --- data/disciplr.db | Bin 32768 -> 0 bytes docs/analytics-storage.md | 60 ++---- eslint.config.js | 4 + package.json | 2 - src/db/database.ts | 441 ++++++++------------------------------ 5 files changed, 120 insertions(+), 387 deletions(-) delete mode 100644 data/disciplr.db diff --git a/data/disciplr.db b/data/disciplr.db deleted file mode 100644 index d5b82a9b1a836fb233e7b7f10649ac1867d18b72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI*PjA~~90zd!Ns}faHK?kLRYV@ap$%KXP823OMUZs{s)Y=?Oxn0uCiWzY{3-F1 zwnO4DG1w7_D-th*fN3A--0kI(~kBettfWpGJ@4 zHa9+NdDJC$hNHesNlkcJkR;&)LIgpO*mIdZ<0Z>hGVu%cQ%tQdT9t&izkjQw{3GNF zKL{(^%CDtAOZQ8wFa5+`Vu1hzAOHafKmY;|fWZBHE~j3;EIz1E`-`4?V2^v$vIlnW zF7>*;<&XP)dvtff=H;C|v)M68r+KAik_ACzvp~YdbI5MHV}4}rkx%z_KWXmWCfCf{ zWandZ=Nj2$r43?KE=Iw0NbQ~#R_RZ6YhUk>_RUs{Ts1#z-fVS<9*5gq>K(YJkM9oq z`#qPs&gpl#V|%@`#Pb&PhTSjOUR`_NV+|r5^A>BuY%@0WjhTj=I^G|5yRPqBBb!Ys zSZ}nB*FHh&**>+#`wk0rrjH~QEoV+7yQLpc^z54q+YxXHw)6vs8 z(q+k=}xV#(z&tMqAQl^Rq^Sg`M%!<$Szckh1Fg znka<5?0?l`^Loo3(_#2J9y)yBR%s?~H|ujSR8JSu_!1Rwwb2tWV= z5P$##AOHafoS#5J)&ErAWOAc*<=^4gkMlY%c!2);+a;THhd7bsMT{@%8JY1=UV E1>)&*JOBUy diff --git a/docs/analytics-storage.md b/docs/analytics-storage.md index a9b85f0c..3bdb6578 100644 --- a/docs/analytics-storage.md +++ b/docs/analytics-storage.md @@ -1,58 +1,42 @@ -# Analytics Storage Migration (SQLite -> PostgreSQL) +# Analytics Storage -## Goal - -Move analytics persistence to PostgreSQL for production workloads while keeping existing `/api/analytics` behavior stable. +Analytics data is persisted in PostgreSQL. The SQLite (`better-sqlite3`) dependency +was removed after the PostgreSQL migration was completed (see issue #334). ## Data model ### `analytics_vault_summary` -- Single-row summary keyed by `id=1` +Single-row summary keyed by `id=1`: + - `total_vaults`, `active_vaults`, `completed_vaults`, `failed_vaults` - `total_locked_capital`, `active_capital`, `success_rate` - `last_updated` ### `analytics_vault_daily_rollups` -- Daily materialized rollup keyed by `bucket_date` -- Same aggregate fields as summary table -- Supports historical analytics and backfill verification - -## Runtime storage modes - -Configure with environment variables: +Daily materialized rollup keyed by `bucket_date` with the same aggregate fields. +Supports historical analytics and backfill verification. -- `ANALYTICS_STORAGE=postgres` enables PostgreSQL reads for analytics summary. -- `ANALYTICS_DUAL_WRITE=true` enables dual-write of summary/rollups to PostgreSQL while SQLite remains active. +## Runtime storage mode -Recommended migration rollout: +Set `ANALYTICS_STORAGE=postgres` to enable PostgreSQL reads for the analytics summary +endpoint. When unset the endpoint returns empty aggregates (safe default for environments +without a live database). -1. Deploy with `ANALYTICS_DUAL_WRITE=true`, `ANALYTICS_STORAGE` unset. -2. Run backfill once and compare summary parity. -3. Switch read path to `ANALYTICS_STORAGE=postgres`. -4. Keep dual-write briefly for safety. -5. Disable dual-write after confidence window. +## Backfill -## Backfill strategy - -- Use `backfillAnalyticsStorage()` from `src/db/database.ts`. -- It initializes analytics tables in PostgreSQL and recomputes: - - global summary row (`analytics_vault_summary`) - - daily rollups (`analytics_vault_daily_rollups`) from `vaults.created_at`. +Call `backfillAnalyticsStorage()` from `src/db/database.ts` to initialise the +PostgreSQL tables and recompute all aggregates from the `vaults` table. ## Validation checklist 1. Run migration: `npm run migrate:latest` -2. Trigger summary recompute/backfill. -3. Verify row counts and totals: - - SQLite `vault_analytics_summary` vs PostgreSQL `analytics_vault_summary` -4. Run contract tests: - - `npm test -- tests/analytics.test.ts` - - `npm test -- tests/jobs.test.ts` - -## Security / privacy considerations - -- Analytics responses remain aggregate-only. -- No additional PII fields are emitted by `/api/analytics`. -- Audit logs and privacy logger behavior are unchanged. +2. Set `ANALYTICS_STORAGE=postgres` and trigger a summary recompute. +3. Verify row counts and totals against the `vaults` table. +4. Run contract tests: `npm test -- tests/analytics.test.ts` + +## Security / privacy + +Analytics responses remain aggregate-only. No PII is emitted by `/api/analytics`. +Audit logs and privacy-logger behaviour are unchanged. diff --git a/eslint.config.js b/eslint.config.js index c1cd718c..82c2fe8a 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -30,6 +30,10 @@ export default [ name: 'ts-node', message: 'ts-node is dropped in favour of tsx and ts-jest. Please do not import or use it.', }, + { + name: 'better-sqlite3', + message: 'SQLite has been removed. Use PostgreSQL via src/db/pool.ts instead.', + }, ], }, ], diff --git a/package.json b/package.json index d6b4ac55..dff032d6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@stellar/stellar-sdk": "^14.5.0", "argon2": "^0.31.0", "bcryptjs": "^3.0.3", - "better-sqlite3": "^11.10.0", "cors": "^2.8.6", "csv-stringify": "^6.6.0", "date-fns": "^4.1.0", @@ -51,7 +50,6 @@ "@eslint/js": "^9.13.0", "@redocly/cli": "^2.29.2", "@types/bcryptjs": "^2.4.6", - "@types/better-sqlite3": "^7.6.13", "@types/cors": "^2.8.19", "@types/debug": "^4.1.13", "@types/express": "^4.17.21", diff --git a/src/db/database.ts b/src/db/database.ts index c8c46f7f..f9a5ed74 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -1,53 +1,8 @@ -import Database, { Database as DatabaseType } from 'better-sqlite3' import type { Pool } from 'pg' -import path from 'path' -import { fileURLToPath } from 'url' -import fs from 'fs' import { subDays, subYears } from 'date-fns' import { utcStartOfDay, utcEndOfDay } from '../utils/timestamps.js' import { getPgPool } from './pool.js' -let _filename: string -let _dirname: string - -try { - // @ts-ignore test runtime shim - _filename = fileURLToPath(import.meta.url) - _dirname = path.dirname(_filename) -} catch { - _filename = __filename - _dirname = __dirname -} - -const dbPath = path.join(_dirname, '../../data/disciplr.db') -const dataDir = path.dirname(dbPath) -if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir, { recursive: true }) -} - -const createFallbackDb = (): DatabaseType => - ({ - pragma: () => undefined, - exec: () => undefined, - prepare: () => ({ - get: () => null, - run: () => undefined, - all: () => [], - }), - close: () => undefined, - }) as unknown as DatabaseType - -export const db: DatabaseType = (() => { - try { - const database = new Database(dbPath) - database.pragma('journal_mode = WAL') - return database - } catch { - console.warn('better-sqlite3 unavailable, using no-op analytics database fallback') - return createFallbackDb() - } -})() - type AnalyticsStatsRow = { total_vaults: number active_vaults: number @@ -69,63 +24,9 @@ export type AnalyticsSummaryRow = { } const analyticsStorage = (process.env.ANALYTICS_STORAGE ?? '').toLowerCase() -const dualWriteEnabled = process.env.ANALYTICS_DUAL_WRITE === 'true' const shouldUsePostgres = analyticsStorage === 'postgres' -const getPoolIfEnabled = (): Pool | null => { - if (!shouldUsePostgres && !dualWriteEnabled) { - return null - } - return getPgPool() -} - -const initializeSqliteSchema = (): void => { - const sqliteDb = getDb() - sqliteDb.exec(` - CREATE TABLE IF NOT EXISTS vaults ( - id TEXT PRIMARY KEY, - creator TEXT NOT NULL, - amount TEXT NOT NULL, - start_timestamp TEXT NOT NULL, - end_timestamp TEXT NOT NULL, - success_destination TEXT NOT NULL, - failure_destination TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'active', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_vaults_status ON vaults(status); - CREATE INDEX IF NOT EXISTS idx_vaults_created_at ON vaults(created_at); - CREATE INDEX IF NOT EXISTS idx_vaults_start_timestamp ON vaults(start_timestamp); - CREATE INDEX IF NOT EXISTS idx_vaults_status_created_at ON vaults(status, created_at); - - CREATE TABLE IF NOT EXISTS vault_analytics_summary ( - id INTEGER PRIMARY KEY CHECK (id = 1), - total_vaults INTEGER NOT NULL DEFAULT 0, - active_vaults INTEGER NOT NULL DEFAULT 0, - completed_vaults INTEGER NOT NULL DEFAULT 0, - failed_vaults INTEGER NOT NULL DEFAULT 0, - total_locked_capital TEXT NOT NULL DEFAULT '0', - active_capital TEXT NOT NULL DEFAULT '0', - success_rate REAL NOT NULL DEFAULT 0, - last_updated TEXT NOT NULL - ); - `) - - const summary = sqliteDb.prepare('SELECT id FROM vault_analytics_summary WHERE id = 1').get() - if (!summary) { - sqliteDb - .prepare(` - INSERT INTO vault_analytics_summary ( - id, total_vaults, active_vaults, completed_vaults, failed_vaults, - total_locked_capital, active_capital, success_rate, last_updated - ) - VALUES (1, 0, 0, 0, 0, '0', '0', 0, datetime('now')) - `) - .run() - } -} +const getPool = (): Pool => getPgPool() const initializePostgresSchema = async (pool: Pool): Promise => { await pool.query(` @@ -158,62 +59,6 @@ const initializePostgresSchema = async (pool: Pool): Promise => { `) } -const getSQLiteStats = (startDate?: string, endDate?: string): AnalyticsStatsRow => { - const sqliteDb = getDb() - const where = startDate && endDate ? 'WHERE created_at >= ? AND created_at <= ?' : '' - const stmt = sqliteDb.prepare(` - SELECT - COUNT(*) as total_vaults, - SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_vaults, - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_vaults, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_vaults, - SUM(CAST(amount AS REAL)) as total_locked_capital, - SUM(CASE WHEN status = 'active' THEN CAST(amount AS REAL) ELSE 0 END) as active_capital - FROM vaults - ${where} - `) - const stats = (startDate && endDate ? stmt.get(startDate, endDate) : stmt.get()) as AnalyticsStatsRow | null - return { - total_vaults: stats?.total_vaults ?? 0, - active_vaults: stats?.active_vaults ?? 0, - completed_vaults: stats?.completed_vaults ?? 0, - failed_vaults: stats?.failed_vaults ?? 0, - total_locked_capital: stats?.total_locked_capital ?? 0, - active_capital: stats?.active_capital ?? 0, - } -} - -const writeSQLiteSummary = (): void => { - const sqliteDb = getDb() - const stats = getSQLiteStats() - const totalCompleted = stats.completed_vaults - const totalFailed = stats.failed_vaults - const successRate = totalCompleted + totalFailed > 0 ? (totalCompleted / (totalCompleted + totalFailed)) * 100 : 0 - - sqliteDb - .prepare(` - UPDATE vault_analytics_summary SET - total_vaults = ?, - active_vaults = ?, - completed_vaults = ?, - failed_vaults = ?, - total_locked_capital = ?, - active_capital = ?, - success_rate = ?, - last_updated = datetime('now') - WHERE id = 1 - `) - .run( - stats.total_vaults, - stats.active_vaults, - stats.completed_vaults, - stats.failed_vaults, - (stats.total_locked_capital ?? 0).toString(), - (stats.active_capital ?? 0).toString(), - successRate, - ) -} - const writePostgresSummary = async (pool: Pool): Promise => { const { rows } = await pool.query(` SELECT @@ -227,43 +72,29 @@ const writePostgresSummary = async (pool: Pool): Promise => { `) const stats = rows[0] ?? { - total_vaults: 0, - active_vaults: 0, - completed_vaults: 0, - failed_vaults: 0, - total_locked_capital: 0, - active_capital: 0, + total_vaults: 0, active_vaults: 0, completed_vaults: 0, failed_vaults: 0, + total_locked_capital: 0, active_capital: 0, } - const totalCompleted = stats.completed_vaults - const totalFailed = stats.failed_vaults - const successRate = totalCompleted + totalFailed > 0 ? (totalCompleted / (totalCompleted + totalFailed)) * 100 : 0 + const successRate = stats.completed_vaults + stats.failed_vaults > 0 + ? (stats.completed_vaults / (stats.completed_vaults + stats.failed_vaults)) * 100 + : 0 await pool.query( - ` - INSERT INTO analytics_vault_summary ( + `INSERT INTO analytics_vault_summary ( id, total_vaults, active_vaults, completed_vaults, failed_vaults, total_locked_capital, active_capital, success_rate, last_updated - ) - VALUES (1, $1, $2, $3, $4, $5, $6, $7, NOW()) - ON CONFLICT (id) DO UPDATE - SET total_vaults = EXCLUDED.total_vaults, - active_vaults = EXCLUDED.active_vaults, - completed_vaults = EXCLUDED.completed_vaults, - failed_vaults = EXCLUDED.failed_vaults, - total_locked_capital = EXCLUDED.total_locked_capital, - active_capital = EXCLUDED.active_capital, - success_rate = EXCLUDED.success_rate, - last_updated = NOW() - `, - [ - stats.total_vaults, - stats.active_vaults, - stats.completed_vaults, - stats.failed_vaults, - stats.total_locked_capital ?? 0, - stats.active_capital ?? 0, - successRate, - ], + ) VALUES (1, $1, $2, $3, $4, $5, $6, $7, NOW()) + ON CONFLICT (id) DO UPDATE SET + total_vaults = EXCLUDED.total_vaults, + active_vaults = EXCLUDED.active_vaults, + completed_vaults = EXCLUDED.completed_vaults, + failed_vaults = EXCLUDED.failed_vaults, + total_locked_capital = EXCLUDED.total_locked_capital, + active_capital = EXCLUDED.active_capital, + success_rate = EXCLUDED.success_rate, + last_updated = NOW()`, + [stats.total_vaults, stats.active_vaults, stats.completed_vaults, stats.failed_vaults, + stats.total_locked_capital ?? 0, stats.active_capital ?? 0, successRate], ) await pool.query(` @@ -273,60 +104,47 @@ const writePostgresSummary = async (pool: Pool): Promise => { ) SELECT DATE(created_at) as bucket_date, - COUNT(*)::int as total_vaults, - SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)::int as active_vaults, - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::int as completed_vaults, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)::int as failed_vaults, - SUM(CAST(amount AS numeric))::numeric(20,7) as total_locked_capital, - SUM(CASE WHEN status = 'active' THEN CAST(amount AS numeric) ELSE 0 END)::numeric(20,7) as active_capital, - CASE - WHEN SUM(CASE WHEN status IN ('completed', 'failed') THEN 1 ELSE 0 END) = 0 THEN 0 - ELSE ( - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::numeric - / SUM(CASE WHEN status IN ('completed', 'failed') THEN 1 ELSE 0 END)::numeric - ) * 100 - END as success_rate, - NOW() as last_updated + COUNT(*)::int, + SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)::int, + SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::int, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)::int, + SUM(CAST(amount AS numeric))::numeric(20,7), + SUM(CASE WHEN status = 'active' THEN CAST(amount AS numeric) ELSE 0 END)::numeric(20,7), + CASE WHEN SUM(CASE WHEN status IN ('completed','failed') THEN 1 ELSE 0 END) = 0 THEN 0 + ELSE (SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::numeric + / SUM(CASE WHEN status IN ('completed','failed') THEN 1 ELSE 0 END)::numeric) * 100 + END, + NOW() FROM vaults GROUP BY DATE(created_at) - ON CONFLICT (bucket_date) DO UPDATE - SET total_vaults = EXCLUDED.total_vaults, - active_vaults = EXCLUDED.active_vaults, - completed_vaults = EXCLUDED.completed_vaults, - failed_vaults = EXCLUDED.failed_vaults, - total_locked_capital = EXCLUDED.total_locked_capital, - active_capital = EXCLUDED.active_capital, - success_rate = EXCLUDED.success_rate, - last_updated = NOW() + ON CONFLICT (bucket_date) DO UPDATE SET + total_vaults = EXCLUDED.total_vaults, + active_vaults = EXCLUDED.active_vaults, + completed_vaults = EXCLUDED.completed_vaults, + failed_vaults = EXCLUDED.failed_vaults, + total_locked_capital = EXCLUDED.total_locked_capital, + active_capital = EXCLUDED.active_capital, + success_rate = EXCLUDED.success_rate, + last_updated = NOW() `) } export function initializeDatabase(): void { - initializeSqliteSchema() - const pool = getPoolIfEnabled() - if (pool) { - void initializePostgresSchema(pool).catch((error) => { - console.warn('PostgreSQL analytics schema initialization failed:', error) - }) - } + const pool = getPool() + void initializePostgresSchema(pool).catch((error) => { + console.warn('PostgreSQL analytics schema initialization failed:', error) + }) } export function closeDatabase(): void { - db.close() - const pool = getPoolIfEnabled() - if (pool) { - void pool.end().catch(() => undefined) - } + void getPool().end().catch(() => undefined) } export function updateAnalyticsSummary(): void { - writeSQLiteSummary() - const pool = getPoolIfEnabled() - if (pool) { - void writePostgresSummary(pool).catch((error) => { - console.warn('PostgreSQL analytics summary update failed:', error) - }) - } + const pool = getPool() + void writePostgresSummary(pool).catch((error) => { + console.warn('PostgreSQL analytics summary update failed:', error) + }) } const mapSummary = (row: Record): AnalyticsSummaryRow => ({ @@ -340,120 +158,62 @@ const mapSummary = (row: Record): AnalyticsSummaryRow => ({ last_updated: String(row.last_updated ?? new Date().toISOString()), }) -export async function readAnalyticsSummary(): Promise { - const pool = shouldUsePostgres ? getPoolIfEnabled() : null - if (pool) { - const { rows } = await pool.query>(` - SELECT - total_vaults, - active_vaults, - completed_vaults, - failed_vaults, - total_locked_capital::text, - active_capital::text, - success_rate::float, - last_updated::text - FROM analytics_vault_summary - WHERE id = 1 - `) - if (rows[0]) { - return mapSummary(rows[0]) - } - } +const emptyAnalyticsSummary = (): AnalyticsSummaryRow => ({ + total_vaults: 0, active_vaults: 0, completed_vaults: 0, failed_vaults: 0, + total_locked_capital: '0', active_capital: '0', success_rate: 0, + last_updated: new Date().toISOString(), +}) - const sqliteDb = getDb() - const summary = sqliteDb.prepare(` - SELECT - total_vaults, - active_vaults, - completed_vaults, - failed_vaults, - total_locked_capital, - active_capital, - success_rate, - last_updated - FROM vault_analytics_summary - WHERE id = 1 - `).get() as Record | undefined +export async function readAnalyticsSummary(): Promise { + if (!shouldUsePostgres) return emptyAnalyticsSummary() - return mapSummary(summary ?? {}) + const pool = getPool() + const { rows } = await pool.query>(` + SELECT total_vaults, active_vaults, completed_vaults, failed_vaults, + total_locked_capital::text, active_capital::text, success_rate::float, last_updated::text + FROM analytics_vault_summary WHERE id = 1 + `) + return rows[0] ? mapSummary(rows[0]) : emptyAnalyticsSummary() } export async function queryVaultStatsByPeriod(startDate: string, endDate: string): Promise { - const pool = shouldUsePostgres ? getPoolIfEnabled() : null - if (pool) { - const { rows } = await pool.query( - ` - SELECT - COUNT(*)::int as total_vaults, - SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)::int as active_vaults, - SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::int as completed_vaults, - SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)::int as failed_vaults, - SUM(CAST(amount AS numeric))::float as total_locked_capital, - SUM(CASE WHEN status = 'active' THEN CAST(amount AS numeric) ELSE 0 END)::float as active_capital - FROM vaults - WHERE created_at >= $1 AND created_at <= $2 - `, - [startDate, endDate], - ) - return rows[0] ?? getSQLiteStats(startDate, endDate) - } - return getSQLiteStats(startDate, endDate) + const pool = getPool() + const { rows } = await pool.query( + `SELECT COUNT(*)::int as total_vaults, + SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END)::int as active_vaults, + SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END)::int as completed_vaults, + SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END)::int as failed_vaults, + SUM(CAST(amount AS numeric))::float as total_locked_capital, + SUM(CASE WHEN status = 'active' THEN CAST(amount AS numeric) ELSE 0 END)::float as active_capital + FROM vaults WHERE created_at >= $1 AND created_at <= $2`, + [startDate, endDate], + ) + return rows[0] ?? { total_vaults: 0, active_vaults: 0, completed_vaults: 0, failed_vaults: 0, + total_locked_capital: 0, active_capital: 0 } } export async function queryVaultStatusBreakdownByPeriod( - startDate: string, - endDate: string, + startDate: string, endDate: string, ): Promise> { - const pool = shouldUsePostgres ? getPoolIfEnabled() : null - if (pool) { - const { rows } = await pool.query<{ status: string; count: string }>( - ` - SELECT status, COUNT(*)::text as count - FROM vaults - WHERE created_at >= $1 AND created_at <= $2 - GROUP BY status - `, - [startDate, endDate], - ) - return rows.map((row) => ({ status: row.status, count: Number(row.count) })) - } - - const rows = getDb() - .prepare( - ` - SELECT status, COUNT(*) as count - FROM vaults - WHERE created_at >= ? AND created_at <= ? - GROUP BY status - `, - ) - .all(startDate, endDate) as Array<{ status: string; count: number }> - return rows + const pool = getPool() + const { rows } = await pool.query<{ status: string; count: string }>( + `SELECT status, COUNT(*)::text as count FROM vaults + WHERE created_at >= $1 AND created_at <= $2 GROUP BY status`, + [startDate, endDate], + ) + return rows.map((r) => ({ status: r.status, count: Number(r.count) })) } export async function queryVaultStatusBreakdownAllTime(): Promise> { - const pool = shouldUsePostgres ? getPoolIfEnabled() : null - if (pool) { - const { rows } = await pool.query<{ status: string; count: string }>(` - SELECT status, COUNT(*)::text as count - FROM vaults - GROUP BY status - `) - return rows.map((row) => ({ status: row.status, count: Number(row.count) })) - } - - return getDb().prepare('SELECT status, COUNT(*) as count FROM vaults GROUP BY status').all() as Array<{ - status: string - count: number - }> + const pool = getPool() + const { rows } = await pool.query<{ status: string; count: string }>( + 'SELECT status, COUNT(*)::text as count FROM vaults GROUP BY status', + ) + return rows.map((r) => ({ status: r.status, count: Number(r.count) })) } export async function backfillAnalyticsStorage(): Promise { - const pool = getPoolIfEnabled() - if (!pool) { - return - } + const pool = getPool() await initializePostgresSchema(pool) await writePostgresSummary(pool) } @@ -464,25 +224,12 @@ export function getTimeRangeFilter(period: string): { startDate: string; endDate let startDate: string switch (period) { - case '7d': - startDate = utcStartOfDay(subDays(now, 7)) - break - case '30d': - startDate = utcStartOfDay(subDays(now, 30)) - break - case '90d': - startDate = utcStartOfDay(subDays(now, 90)) - break - case '1y': - startDate = utcStartOfDay(subYears(now, 1)) - break - default: - return { startDate: new Date(0).toISOString(), endDate } + case '7d': startDate = utcStartOfDay(subDays(now, 7)); break + case '30d': startDate = utcStartOfDay(subDays(now, 30)); break + case '90d': startDate = utcStartOfDay(subDays(now, 90)); break + case '1y': startDate = utcStartOfDay(subYears(now, 1)); break + default: return { startDate: new Date(0).toISOString(), endDate } } return { startDate, endDate } } - -function getDb(): DatabaseType { - return db -} \ No newline at end of file