docs: replace placeholder README with v0.1.0 docs
Install via git+https with pinned tag, quick-start, API reference for the 4 runtime exports + the CLI sub-path, genesis-stamping explainer, the per-consumer probe-table table for the six fleet consumers, security note on the trusted-SQL boundary.
This commit is contained in:
162
README.md
162
README.md
@@ -1,3 +1,161 @@
|
||||
# sqlite-migrate
|
||||
# bchen-sqlite-migrate
|
||||
|
||||
Tiny SQLite migration runner: version-numbered, checksum-verified, idempotent. Used across multiple projects.
|
||||
Tiny, forward-only SQLite migration runner for `better-sqlite3`: numbered `.sql` files, sha256 checksum verification, idempotent re-apply, transactional per-migration, and genesis-stamping for pre-existing prod DBs.
|
||||
|
||||
Lifted from the [authd](https://gitea.bchen.dev/brendan/authd) auth service (PR #14, merge `ced21ab`) and generalized for cross-project use across the fleet.
|
||||
|
||||
## Install
|
||||
|
||||
Public repo — no auth needed:
|
||||
|
||||
```bash
|
||||
npm install git+https://gitea.bchen.dev/brendan/sqlite-migrate.git#v0.1.0
|
||||
```
|
||||
|
||||
Pin to a tag (`#v0.1.0`), not a branch.
|
||||
|
||||
Peer dependency:
|
||||
|
||||
```json
|
||||
"peerDependencies": {
|
||||
"better-sqlite3": ">=11 <13"
|
||||
}
|
||||
```
|
||||
|
||||
Node 20+. ESM-only. Ships pre-compiled `dist/`, so no build step is needed in consumers.
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import Database from 'better-sqlite3';
|
||||
import { applyMigrations } from 'bchen-sqlite-migrate';
|
||||
|
||||
const db = new Database('./app.db');
|
||||
const summary = applyMigrations(db, './migrations', {
|
||||
genesisProbeTable: 'users',
|
||||
logger: console.log,
|
||||
});
|
||||
// summary: { applied, alreadyApplied, pending, stamped }
|
||||
```
|
||||
|
||||
Migration files live in a single flat directory and follow the naming convention `NNNN_name.sql` (4+ digits, snake_case name, `.sql` extension), e.g. `0001_init.sql`, `0002_articles_user_scope.sql`.
|
||||
|
||||
## API
|
||||
|
||||
### `applyMigrations(db, migrationsDir, opts?) => MigrationSummary`
|
||||
|
||||
Apply all pending migrations in version order. Each migration runs inside a transaction; failure rolls back. Re-running is idempotent — already-applied migrations are skipped after a checksum compare; mismatch throws.
|
||||
|
||||
```ts
|
||||
interface ApplyOptions {
|
||||
/** Per-message logger (info + WARN). Default no-op. */
|
||||
logger?: (msg: string) => void;
|
||||
/** Force-stamp the 0001 migration without executing it. */
|
||||
stampGenesis?: boolean;
|
||||
/** Default 'users'. Probe table for "is this a pre-existing prod DB?". */
|
||||
genesisProbeTable?: string;
|
||||
}
|
||||
|
||||
interface MigrationSummary {
|
||||
applied: number; // ran successfully this call
|
||||
pending: number; // always 0; kept for forward-compat
|
||||
alreadyApplied: number; // already in schema_migrations
|
||||
stamped: string[]; // versions stamped without execution
|
||||
}
|
||||
```
|
||||
|
||||
### `listMigrations(migrationsDir) => MigrationFile[]`
|
||||
|
||||
Reads + parses `NNNN_*.sql` files into `{ version, name, checksum, body }`. Throws on filenames that don't match `^(\d{4,})_([a-z0-9_]+)\.sql$`.
|
||||
|
||||
### `readAppliedRows(db) => MigrationRow[]`
|
||||
|
||||
Reads `schema_migrations`. Returns `[]` if the table doesn't exist.
|
||||
|
||||
### `stampMigration(db, migrationsDir, version) => MigrationFile`
|
||||
|
||||
Marks `version` as applied without running its body. Throws if already stamped. Useful for retrofitting an existing-prod DB to a new migration scheme.
|
||||
|
||||
## Genesis stamping
|
||||
|
||||
When applying against a DB that has tables but no `schema_migrations` row, the runner can mark the `0001` migration as "applied" without executing its body. This lets you adopt the migration system on an existing prod DB without re-running `CREATE TABLE` against tables that already exist.
|
||||
|
||||
Two triggers:
|
||||
|
||||
1. `applyMigrations(db, dir, { stampGenesis: true })` — explicit opt-in. Typically wired to an env var:
|
||||
|
||||
```bash
|
||||
DB_MIGRATIONS_STAMP_GENESIS=1 node ./scripts/db-migrate.mjs
|
||||
```
|
||||
|
||||
2. `genesisProbeTable` — auto-detection. If this table exists in the DB and `schema_migrations` doesn't, stamp 0001. Default `'users'`. Pass your project's first-table name to match a non-authd shape.
|
||||
|
||||
When stamping fires, the runner emits one WARN line via `logger`:
|
||||
|
||||
```
|
||||
WARN genesis-stamp: marked 0001_init as applied without executing (detected pre-existing 'users' table)
|
||||
```
|
||||
|
||||
Or, when triggered explicitly:
|
||||
|
||||
```
|
||||
WARN genesis-stamp: marked 0001_init as applied without executing (stampGenesis option set)
|
||||
```
|
||||
|
||||
## Per-consumer call signatures (fleet)
|
||||
|
||||
| Consumer | `genesisProbeTable` |
|
||||
|-------------|---------------------|
|
||||
| authd | `'users'` (default — can omit) |
|
||||
| buchinese | `'sessions'` |
|
||||
| inventory | TBD — verify at adoption |
|
||||
| nanodrop | `'users'` |
|
||||
| dashcam | `'sessions'` |
|
||||
| movement | TBD — verify at adoption |
|
||||
|
||||
## CLI helper
|
||||
|
||||
The package exports a wrapper at the `bchen-sqlite-migrate/cli` sub-path so each consumer writes a ~5-line shim:
|
||||
|
||||
```ts
|
||||
// scripts/db-migrate.ts
|
||||
import { runMigrateCli } from 'bchen-sqlite-migrate/cli';
|
||||
import { openDb } from './_db-open.js';
|
||||
|
||||
process.exit(runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: new URL('../db/migrations/', import.meta.url).pathname,
|
||||
command: process.argv[2] as 'migrate' | 'status' | 'stamp',
|
||||
version: process.argv[3],
|
||||
stampGenesis: process.env.DB_MIGRATIONS_STAMP_GENESIS === '1',
|
||||
genesisProbeTable: 'users',
|
||||
}));
|
||||
```
|
||||
|
||||
`runMigrateCli` **returns** an exit code rather than calling `process.exit` itself — the consumer is responsible for `process.exit(...)`. The wrapper closes the DB before returning. This keeps the helper testable (you can capture stdout/stderr via `PassThrough` streams).
|
||||
|
||||
Three commands:
|
||||
|
||||
- `migrate` — apply pending migrations.
|
||||
- `status` — print each migration's state (`applied:<v>` / `pending:<v>` / `checksum-mismatch:<v>`). Exit code `2` if any mismatched.
|
||||
- `stamp <version>` — manually mark a version as applied without running its body.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
git clone ssh://git@gitea.bchen.dev:2222/brendan/sqlite-migrate.git
|
||||
cd sqlite-migrate
|
||||
npm install
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
`dist/` is committed (consumers install via `git+https://...#v0.1.0` and need the pre-built JS). CI verifies `dist/` is in sync with `src/` via `git diff --exit-code dist/`. Re-run `npm run build` before committing if you touched `src/`.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](./LICENSE).
|
||||
|
||||
## Security
|
||||
|
||||
This is a server-side library with no HTTP surface and no untrusted input. `db.exec(file.body)` runs each migration's SQL verbatim — **by design**, since migrations are trusted, committed-to-repo SQL. Do not point `migrationsDir` at a path that can be written to by untrusted users.
|
||||
|
||||
Reference in New Issue
Block a user