Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/eql-preflight-deferred-grants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'stash': minor
---

EQL installs no longer abort on managed platforms whose database role is not `postgres`, and a new `stash eql preflight` command reports role capability before anything is attempted.

- `stash eql install` (and `eql upgrade`) now run the EQL v3 bundle in its own transaction and the Supabase role grants after it commits, so a grants failure can no longer roll back a working install. When the connecting role is not a member of `postgres` (e.g. Lovable's `sandbox_exec`), the three owner-scoped `ALTER DEFAULT PRIVILEGES FOR ROLE postgres` statements are skipped and the install completes without them — they are optional (they only cover EQL objects `postgres` might later create outside stash tooling, and stash re-grants every object on each install/upgrade); the SQL is printed as "Optional SQL — requires postgres" for operators who want it. Every plain `GRANT` still runs. Previously that single refused statement rolled back the entire install (~194 functions).
- Re-running `stash eql install` on an already-installed Supabase database now re-applies the role grants (idempotent) instead of exiting early, so an install whose grants step failed heals on a plain re-run.
- The migration generated by `stash eql migration --supabase` wraps the owner-scoped statements in a `pg_has_role` guard, so it applies cleanly whatever role the project's migration runner uses — a non-member role skips them instead of aborting the whole migration.
- New read-only `stash eql preflight` (`--json` for agents): reports `current_user`, superuser, membership of `postgres` (guarded for databases with no `postgres` role), `CREATE` on the database and on `public` (guarded for databases without a `public` schema), `pgcrypto` presence *and placement* (a pgcrypto outside `extensions`/`public` aborts the bundle, even for superusers), and the EQL v3 schemas' presence and drop-ownership (a reinstall begins with `DROP SCHEMA ... CASCADE`) — each blocked row naming the statement it blocks. Exits 1 on blocking gaps; membership of `postgres` never blocks. `--json` stdout is pure JSON in every outcome: `{ status: 'ok' | 'blocked', ... }`, or the shared `{ status: 'error', code, message }` envelope — including when no DATABASE_URL is configured. The same check runs at the head of `eql install`.
- Install failure messages now state recoverability: a bundle failure says nothing was applied (rolled back); a grants failure says the install itself was kept.
- Library surface: `EQLInstaller.preflight()` (rich `PreflightResult`) supersedes `checkPermissions()`, which remains as a deprecated adapter with its `PermissionCheckResult` shape unchanged — no breaking change for existing `stash@1.x` consumers. `install()` now returns `InstallResult` with the skipped SQL, if any, and `applySupabaseGrants()` re-applies the grants alone. The exact `SUPABASE_PERMISSIONS_SQL_V3` block is unchanged byte-for-byte; new exports expose its immediate (`SUPABASE_IMMEDIATE_GRANTS_SQL_V3`), owner-scoped (`SUPABASE_DEFAULT_PRIVILEGES_SQL_V3`), guarded (`SUPABASE_GUARDED_DEFAULT_PRIVILEGES_SQL_V3`), and migration (`SUPABASE_MIGRATION_GRANTS_SQL_V3`) forms.
18 changes: 12 additions & 6 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,23 +448,29 @@ import { EQLInstaller } from 'stash'

const installer = new EQLInstaller({ databaseUrl: process.env.DATABASE_URL! })

const permissions = await installer.checkPermissions()
if (!permissions.ok) {
console.error('Missing permissions:', permissions.missing)
const preflight = await installer.preflight()
if (!preflight.ok) {
console.error('Blocking gaps:', preflight.missing)
process.exit(1)
}

if (!(await installer.isInstalled())) {
await installer.install({ supabase: true })
const { deferredGrantsSql } = await installer.install({ supabase: true })
// Non-null when the connecting role is not a member of `postgres`: the
// optional owner-scoped default-privilege statements, skipped. The install
// is complete without them.
if (deferredGrantsSql) console.log(deferredGrantsSql)
}
```

| Method | Returns | Description |
|--------|---------|-------------|
| `checkPermissions()` | `Promise<PermissionCheckResult>` | Check required database permissions |
| `preflight()` | `Promise<PreflightResult>` | Read-only role-capability report (superuser, membership of `postgres`, CREATE privileges, pgcrypto placement, EQL schema presence/ownership) |
| `checkPermissions()` | `Promise<PermissionCheckResult>` | **Deprecated** — thin adapter over `preflight()`; will be removed in the next major |
| `isInstalled()` | `Promise<boolean>` | Check if the EQL v3 schemas exist |
| `getInstalledVersion()` | `Promise<string \| null>` | Get the installed EQL version |
| `install(options?)` | `Promise<void>` | Execute the EQL install SQL in a transaction |
| `install(options?)` | `Promise<InstallResult>` | Install the EQL bundle (its own transaction), then the Supabase grants (outside it, so a grants failure keeps the install) |
| `applySupabaseGrants()` | `Promise<InstallResult>` | Re-apply the Supabase role grants alone (idempotent) |

Install options: `supabase`.

Expand Down
Loading
Loading