packages/bruno-sqlite/README.md
SQLite storage for the Bruno API client. You author migrations as .ts and statements as .sql; the package compiles them into a typed, cached data layer for the Electron main process and the React renderer.
@usebruno/sqlite (or /node) — main process. Owns the DB and runs statements.@usebruno/sqlite/web — renderer. React Query hooks that call statements over IPC.Peer deps for the web layer: react 19, @tanstack/react-query 5. The node layer uses the built-in node:sqlite.
npm run migration:new --workspace=packages/bruno-sqlite # prompts for a name
This scaffolds a sequence-prefixed migrations/<seq>_<name>.ts file exporting up and down. Each returns the SQL to apply or revert the migration (a plain string also works):
export const up = (): string => {
return 'CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT);';
}
export const down = (): string => {
return 'DROP TABLE users;';
}
Migrations apply automatically (in prefix order) when the DB opens, and their content is hashed into a _migrations table so an already-applied migration can't be silently edited.
npm run migration:verify replays every migration against a throwaway VACUUM INTO copy of a real database, so it exercises them over actual data without touching the source DB. Point it at the DB with an absolute DB_PATH:
cp .env.example .env # then set DB_PATH to an absolute path
npm run migration:verify --workspace=packages/bruno-sqlite
It fails if a migration errors, or if a migration's content no longer matches what was recorded when it was first applied to that DB.
Write them in statements/*.sql — multiple per file. Each starts with a -- name: annotation, and params are named (@param):
-- name: list_users :many
SELECT id, name, email FROM users ORDER BY name;
-- name: get_user :one
SELECT id, name, email FROM users WHERE id = @id;
-- name: create_user :exec
INSERT INTO users (name, email) VALUES (@name, @email);
| annotation | runs as | returns |
|---|---|---|
:one | single row | row or undefined |
:many | rows | array |
:exec | write | { changes, lastInsertRowid } |
Run npm run generate (or build) to compile statements into the typed registry.
const { createDatabase } = require('@usebruno/sqlite');
const { db, statements } = createDatabase('/path/to/bruno.db');
statements.execute('create_user', { name: 'Ada', email: '[email protected]' });
const users = statements.execute('list_users'); // no params
const ada = statements.execute('get_user', { id: 1 });
// db.close() on shutdown
registerSQLiteIpc(ipcMain, statements) exposes every statement to the renderer over IPC.
Wrap the app once (Electron's window.ipcRenderer works as the bridge):
import { SQLiteProvider } from '@usebruno/sqlite/web';
<SQLiteProvider bridge={window.ipcRenderer}>
<App />
</SQLiteProvider>
Then read and write by statement name:
import { useSqliteQuery, useSqliteMutation } from '@usebruno/sqlite/web';
const { data, isFetching } = useSqliteQuery('list_users');
const one = useSqliteQuery('get_user', { id: 5 });
const create = useSqliteMutation('create_user');
create.mutate({ name: 'Ada', email: '[email protected]' });
useSqliteQuery(name, params?) returns the React Query result (data, isFetching, error, refetch, …); results are cached.useSqliteMutation(name) returns { mutate, mutateAsync, … }.Params are always an object keyed by the named parameters (no positional/array binding).
npm run build --workspace=packages/bruno-sqlite # generate + bundle + types
npm run watch --workspace=packages/bruno-sqlite # rebuild on change
npm run test --workspace=packages/bruno-sqlite # jest
.sql files are the only source of truth committed; src/generated/ and dist/ are git-ignored and produced by the build.