feat: add SQLite as alternative database backend (#64)
* feat(db): add @libsql/client dependency for SQLite support
* feat(db): add SQLite schema file
* feat(db): add role_permissions table to Postgres schema (permissions column still present)
* feat(db): URL-based driver detection in createDb (postgres vs SQLite)
* feat(appview): add dialect-specific Drizzle configs and update db scripts
* feat(db): migration 0011 — add role_permissions table
* feat(appview): add migrate-permissions data migration script
Copies permissions from roles.permissions[] into the role_permissions
join table before the column is dropped in migration 0012.
* feat(db): migration 0012 — drop permissions column from roles (data already in role_permissions)
* feat(db): add SQLite migrations (single clean initial migration)
* feat(appview): update checkPermission and getUserRole to use role_permissions table
* feat(appview): update indexer to store role permissions in role_permissions table
- Remove permissions field from roleConfig.toInsertValues and toUpdateValues
(the permissions text[] column no longer exists on the roles table)
- Add optional afterUpsert hook to CollectionConfig for post-row child writes
- Implement roleConfig.afterUpsert to delete+insert into role_permissions
within the same transaction, using .returning({ id }) to get the row id
- Update genericCreate and genericUpdate to call afterUpsert when defined
- Rewrite indexer-roles test assertions to query role_permissions table
- Remove permissions field from direct db.insert(roles) test setup calls
* feat(appview): update admin routes to return permissions from role_permissions table
- GET /api/admin/roles: removed roles.permissions from select, enriches each
role with permissions fetched from role_permissions join table via Promise.all
- GET /api/admin/members/me: removed roles.permissions from join select, adds
roleId to select, then fetches permissions separately from role_permissions
- Updated all test files to remove permissions field from db.insert(roles).values()
calls, replacing with separate db.insert(rolePermissions).values() calls after
capturing the returned role id via .returning({ id: roles.id })
- Fixed admin-backfill.test.ts mock count: checkPermission now makes 3 DB
selects (membership + role + role_permissions) instead of 2
- Updated seed-roles.ts CLI step to insert permissions into role_permissions
table separately instead of the removed permissions column
* feat(appview): update test context to support SQLite via createDb factory
- Replace hardcoded postgres.js/drizzle-orm/postgres-js with createDb() from @atbb/db,
which auto-detects the URL prefix (postgres:// vs file:) and selects the right driver
- Add runSqliteMigrations() to @atbb/db so migrations run via the same drizzle-orm
instance used by createDb(), avoiding cross-package module boundary issues
- For SQLite in-memory mode, upgrade file::memory: to file::memory:?cache=shared so
that @libsql/client's transaction() handoff (which sets #db=null and lazily reconnects)
reattaches to the same shared in-memory database rather than creating an empty new one
- For SQLite cleanDatabase(): delete all rows without WHERE clauses (isolated in-memory DB)
- For Postgres cleanDatabase(): retain existing DID-based patterns
- Remove sql.end() from cleanup() — createDb owns the connection lifecycle
- Fix boards.test.ts and categories.test.ts "returns 503 on database error" tests to use
vi.spyOn() instead of relying on sql.end() to break the connection
- Replace count(*)::int (Postgres-specific cast) with Drizzle's count() helper in admin.ts
so the backfill/:id endpoint works on both Postgres and SQLite
* feat: add docker-compose.sqlite.yml for SQLite deployments
* feat(nix): add database.type option to NixOS module (postgresql | sqlite)
* feat(nix): include SQLite migrations and dialect configs in Nix package output
* feat(devenv): make postgres optional via mkDefault, document SQLite alternative
* refactor(appview): embed data migration into 0012 postgres migration
Copy permissions array into role_permissions join table inside the same
migration that drops the column. ON CONFLICT DO NOTHING keeps the script
idempotent for DBs that already ran migrate-permissions.ts manually.
* fix(appview): guard against out-of-order UPDATE in indexer; populate role_permissions in mod tests
- indexer.ts: add `if (!updated) return` before afterUpsert call so an
out-of-order firehose UPDATE that matches zero rows (CREATE not yet
received) doesn't crash with TypeError on `updated.id`
- mod.test.ts: add rolePermissions inserts for all 4 role setups so
test DB state accurately reflects the permissions each role claims
to have (ban admin: banUsers; lock/unlock mod: lockTopics)
* fix(appview): replace fabricated editPosts permission with real lockTopics in admin test
space.atbb.permission.editPosts is not defined in the lexicon or any
default role. Swap it for space.atbb.permission.lockTopics so the
GET /api/admin/members/me test uses a real permission value and would
catch a regression that silently dropped a real permission from a role.