Fresh `mosaic gateway install` (npm) left the gateway DB schema empty —
sign-in 500'd with `relation "users" does not exist`, and every entry
point (auth, bootstrap setup) failed because they all query the users
table first. Five stacked bugs on the local (PGlite) tier:
1. `packages/db/package.json` `files: ["dist"]` excluded the `drizzle/`
SQL migrations from the published tarball.
2. `runMigrations()` only supports postgres-js — unusable for embedded
PGlite.
3. `apps/gateway/src/database/database.module.ts` never invoked
migrations at startup.
4. `createPgliteDb` didn't load pgvector, so migration 0001's
`CREATE EXTENSION vector` failed.
5. Drizzle's PG migrator wraps every migration in one outer
transaction, which trips Postgres' `check_safe_enum_use` on
migration 0009 (`ALTER TYPE ADD VALUE 'pending'` → `SET DEFAULT
'pending'` in the same tx).
Changes:
- Ship `drizzle/` in the published tarball.
- `createPgliteDb` loads `@electric-sql/pglite/vector`.
- New `runPgliteMigrations(handle)` walks the Drizzle journal and
runs each statement-breakpoint chunk through PGlite's `client.exec()`
(autocommit per statement). Records into `drizzle.__drizzle_migrations`
for interop with the postgres-js path. Per-statement try/catch
surfaces which statement of which migration failed.
- `DatabaseModule` runs migrations in `OnModuleInit` before
`app.listen()`. Local tier: explicit `runPgliteMigrations` then
`storageAdapter.migrate()`. Postgres tier: just `storageAdapter.migrate()`,
which already calls `runMigrations(url)` internally — no double-call.
- Removed `packages/storage/src/test-utils/pglite-with-vector.ts`. The
"intentionally not exported" rationale is moot now that migration
0001 forces pgvector load anyway. The integration test uses
`createPgliteDb` + `runPgliteMigrations` from `@mosaicstack/db`.
Tests: BetterAuth tables exist after migrate; idempotent (re-runs 0009);
partial-failure surfaces statement-level context and leaves no ledger row.
QA on a fresh PGlite install:
- `Applying PGlite schema migrations...` then `Initializing storage
adapter (pglite)...` in startup log.
- `GET /api/bootstrap/status` → `{"needsSetup":true}` HTTP 200 (was 500).
- `POST /api/bootstrap/setup` reaches Zod validator (was 500).
Scope: this PR fixes the local (PGlite) tier. Postgres-tier first
install still has the outer-transaction problem and a journal ordering
bug (0009's `when` < 0008's). Documented inline as TODO and in the
scratchpad — needs a separate change with real-Postgres validation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
71 lines
2.5 KiB
TypeScript
71 lines
2.5 KiB
TypeScript
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { sql } from 'drizzle-orm';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { createPgliteDb } from './client-pglite.js';
|
|
import { runPgliteMigrations } from './migrate.js';
|
|
import type { DbHandle } from './client.js';
|
|
|
|
interface PgliteExec {
|
|
exec(query: string): Promise<unknown>;
|
|
}
|
|
|
|
describe('runPgliteMigrations', () => {
|
|
let dataDir: string;
|
|
let handle: DbHandle;
|
|
|
|
beforeEach(() => {
|
|
dataDir = mkdtempSync(join(tmpdir(), 'mosaic-db-migrate-test-'));
|
|
handle = createPgliteDb(dataDir);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await handle.close();
|
|
rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('creates the BetterAuth tables required by the gateway', async () => {
|
|
await runPgliteMigrations(handle);
|
|
|
|
const result = (await handle.db.execute(sql`
|
|
SELECT table_name FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name
|
|
`)) as unknown as { rows: Array<{ table_name: string }> };
|
|
|
|
const tables = result.rows.map((r) => r.table_name);
|
|
|
|
// Auth tables — required for sign-in / bootstrap to function.
|
|
expect(tables).toContain('users');
|
|
expect(tables).toContain('sessions');
|
|
expect(tables).toContain('accounts');
|
|
expect(tables).toContain('verifications');
|
|
|
|
// Schema sanity check — admin token table consumed by mosaic gateway config.
|
|
expect(tables).toContain('admin_tokens');
|
|
});
|
|
|
|
it('is idempotent — running twice does not error', async () => {
|
|
await runPgliteMigrations(handle);
|
|
await expect(runPgliteMigrations(handle)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('surfaces statement-level error context on failure and leaves no ledger row', async () => {
|
|
// Pre-create a `users` table that conflicts with migration 0000's CREATE TABLE,
|
|
// forcing it to fail without IF NOT EXISTS.
|
|
const client = (handle.db as unknown as { $client: PgliteExec }).$client;
|
|
await client.exec('CREATE TABLE users (sentinel text)');
|
|
|
|
await expect(runPgliteMigrations(handle)).rejects.toThrow(
|
|
/migration hash=[a-f0-9]+ statement #\d+ failed/,
|
|
);
|
|
|
|
// Ledger should be empty — partial application must not pretend to be complete.
|
|
const ledger = (await handle.db.execute(
|
|
sql`SELECT count(*)::int AS count FROM drizzle.__drizzle_migrations`,
|
|
)) as unknown as { rows: Array<{ count: number }> };
|
|
expect(ledger.rows[0]?.count).toBe(0);
|
|
});
|
|
});
|