mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-10-05 19:28:26 +00:00
Completes the mint-identity work. A mint url is a network locator and mints move, but the url had become the de facto foreign key for nearly every persisted row, so a url edit had to fan out across the schema and mostly did not. Each table now references the mint by an identity that a move cannot disturb, and setMintUrl shrinks to the two things that genuinely hold a url: Mint.mintUrl and the proofs.mintUrl cache. Which identity, per table mintId (Mint.id) where the row needs the mint itself: - onchain_mint_quotes. The critical one: a quote's address stays creditable for as long as the mint exists (rows are never deleted), so the reference has to outlive a move. It followed row.mintUrl, and the watcher swallows errors by design — a renamed mint stranded deposits permanently and silently. - reservations. A url edit racing an open send did not merely misfile the proofs; commitReservation resolved the mint by url, so it threw "Mint not found" and aborted the commit of an operation the mint had already performed. - transactions. `mint` was two things at once, switched by status: a historical record of where a finished payment happened, AND a live pointer dialled for an open one. That conflation is why a rename had to rewrite in-flight rows — rewriting the very column that records the past. mintId takes the identity job, so `mint` is frozen as history and the status-scoped rewrite is retired. No reference at all where the row already has a parent: - inflight_requests, melt_recovery are CHILD rows of a transaction (their primary key IS transactionId), so the parent owns "which mint". Their mintUrl and keysetId copies had ZERO readers — the keyset that is used comes from inside meltPreview. Both dropped; the one mint-scoped query joins through transactions.mintId. Nothing left to go stale. Mint.id gets referential authority only, never identity authority: findById answers "which mint is this row about?" and must never answer "are these the same mint?" — it is random and unrelated to the keys, so that question stays with the keysets. The backfills are IS NULL-guarded so a resolved row can never be re-pointed at whichever mint now answers an old url. Backfills run from JS (v38 seed), not SQL: mints live in the MST/MMKV snapshot, so nothing in SQL can map url -> id. Matching on url is trustworthy at exactly that moment and no other — until now a url could not change without these rows being rewritten to match. The join is spent once, at rest, instead of on every rename. Migration-system fixes found along the way - _dbVersion is now DERIVED from the migration list. It was a hand-maintained literal, and it was already wrong: it said 33 while migration 34 existed, so 34 would never have run. The failure is silent and asymmetric — fresh installs build from schema.ts and are fine, while upgrading devices land on a schema the code does not have. dbMigrationRegistry.test.ts pins this and the ordering invariants. - Migrations 26/28/29/31 built their tables from the LIVE schema constants. A device replaying them would get today's shape, and the later ALTER adding the column would fail with "duplicate column name" — breaking upgrades from exactly the versions those migrations serve. Historical shapes are frozen locally now, and the schema.ts header no longer recommends the sharing. - rootStoreModelVersion was left at 37 while the seed guarded on < 38, so the backfill would have re-run on every launch forever. Tests: 421 pass. Six hand-mirrored suites had drifted from the schema they claim to mirror; transactionsMintUrl.test.ts was worse — still passing while testing a function this commit deletes, so it is removed. The mirroring pattern is worth revisiting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
48 lines
1.9 KiB
TypeScript
48 lines
1.9 KiB
TypeScript
/**
|
|
* Invariants of the migration registry itself (services/db/migrations.ts).
|
|
*
|
|
* These guard the wiring rather than any one migration's SQL. The failure they
|
|
* exist for is silent and asymmetric: a migration whose version is not below
|
|
* `_dbVersion` never runs (the runner applies only `currentVersion <
|
|
* migration.version`, then stores `_dbVersion`), so upgrading devices end up on a
|
|
* schema the code does not have and fail later with a missing column — while a
|
|
* FRESH install is perfectly fine, because it builds from schema.ts and skips
|
|
* migrations entirely. That asymmetry is what makes it easy to ship.
|
|
*
|
|
* migrations.ts imports only a type from ./connection (elided at runtime), so it
|
|
* loads without the native op-sqlite module.
|
|
*
|
|
* @jest-environment node
|
|
*/
|
|
jest.mock('../src/services/logService', () => ({
|
|
log: {debug: jest.fn(), error: jest.fn(), info: jest.fn(), trace: jest.fn(), warn: jest.fn()},
|
|
}))
|
|
|
|
import {_dbVersion, MIGRATIONS} from '../src/services/db/migrations'
|
|
|
|
describe('migration registry', () => {
|
|
test('_dbVersion equals the newest migration version', () => {
|
|
const newest = Math.max(...MIGRATIONS.map(m => m.version))
|
|
expect(_dbVersion).toBe(newest)
|
|
})
|
|
|
|
test('every migration version is unique', () => {
|
|
const versions = MIGRATIONS.map(m => m.version)
|
|
expect(new Set(versions).size).toBe(versions.length)
|
|
})
|
|
|
|
test('migrations are in ascending order', () => {
|
|
// The runner concatenates matching migrations in array order and runs them as
|
|
// one batch, so the array order IS the execution order — a later-versioned
|
|
// entry placed earlier would apply out of sequence.
|
|
const versions = MIGRATIONS.map(m => m.version)
|
|
expect(versions).toEqual([...versions].sort((a, b) => a - b))
|
|
})
|
|
|
|
test('no migration is empty', () => {
|
|
for (const m of MIGRATIONS) {
|
|
expect(m.queries.length).toBeGreaterThan(0)
|
|
}
|
|
})
|
|
})
|