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
22 changes: 22 additions & 0 deletions .changeset/eql-migration-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'stash': minor
---

Add `stash eql migration` — generate an EQL **v3** install migration for your ORM
instead of running the SQL directly against the database (`stash eql install`).
Migration-first is the preferred path: the install lands in your migration history
and ships to every environment through the ORM's own migrate step.

```bash
stash eql migration --drizzle # Drizzle custom migration
stash eql migration --drizzle --supabase # also grants eql_v3 to anon/authenticated/service_role
```

The migration carries the CLI's bundled v3 install SQL (one source of truth) plus
the `cs_migrations` tracking schema, so a single `drizzle-kit migrate` covers
everything `stash encrypt …` needs. `--supabase` appends the `eql_v3` +
`eql_v3_internal` role grants for PostgREST/RLS access.

`--prisma` is registered but not available yet — the Prisma Next migration
emitter is a follow-up (tracked in cipherstash/stack#690) that will let
prisma-next drop its baked install baseline. It fails with a pointer for now.
15 changes: 15 additions & 0 deletions packages/cli/src/bin/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Commands:
telemetry <sub> Manage anonymous usage analytics (status, enable, disable)

eql install Scaffold stash.config.ts (if missing) and install EQL extensions
eql migration Generate an EQL v3 install migration for your ORM (Drizzle)
eql upgrade Upgrade EQL extensions to the latest version
eql status Show EQL installation status

Expand Down Expand Up @@ -232,6 +233,20 @@ async function runEqlCommand(
case 'install':
await runInstall(flags, values)
break
case 'migration': {
const { eqlMigrationCommand } = await import(
'../commands/eql/migration.js'
)
await eqlMigrationCommand({
drizzle: flags.drizzle,
prisma: flags.prisma,
supabase: flags.supabase,
name: values.name,
out: values.out,
dryRun: flags['dry-run'],
})
break
}
case 'upgrade':
await runUpgrade(flags, values)
break
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,45 @@ export const registry: CommandGroup[] = [
DATABASE_URL_FLAG,
],
},
{
name: 'eql migration',
summary:
'Generate an EQL v3 install migration for your ORM (Drizzle now; Prisma Next soon)',
examples: [
'eql migration --drizzle',
'eql migration --drizzle --supabase',
],
flags: [
{
name: '--drizzle',
description:
'Emit a Drizzle custom migration containing the EQL v3 install SQL.',
},
{
name: '--prisma',
description:
'Emit a Prisma Next migration — not available yet; the emitter is a follow-up tracked in cipherstash/stack#690.',
},
{
name: '--supabase',
description:
'Append the Supabase role grants (eql_v3 + eql_v3_internal for anon/authenticated/service_role).',
},
{
name: '--name',
value: '<name>',
description:
'Name for the generated migration (Drizzle). Letters, numbers, dashes, underscores only. Defaults to `install-eql`.',
},
{
name: '--out',
value: '<path>',
description:
'Directory drizzle-kit writes the migration into (passed to `drizzle-kit generate --out`). Defaults to `drizzle`; set it to match your drizzle.config.ts.',
},
DRY_RUN_FLAG,
],
},
{
name: 'eql upgrade',
summary: 'Upgrade EQL extensions to the latest version',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { findGeneratedMigration } from '../install.js'

/**
* `findGeneratedMigration` was promoted to public API by the `eql migration`
* command and now has two consumers (`db install --drizzle` and
* `eql migration --drizzle`), so its branches are pinned directly — a change for
* one consumer must not silently break the other.
*/
describe('findGeneratedMigration', () => {
let dir: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'stash-find-migration-'))
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
})

it('throws when the out directory does not exist', async () => {
await expect(
findGeneratedMigration(join(dir, 'nope'), 'install-eql'),
).rejects.toThrow(/output directory not found/)
})

it('throws when no .sql file matches the migration name', async () => {
writeFileSync(join(dir, '0000_other.sql'), '')
await expect(findGeneratedMigration(dir, 'install-eql')).rejects.toThrow(
/Could not find a migration matching "install-eql"/,
)
})

it('returns the highest-numbered match, ignoring non-.sql and non-matching entries', async () => {
for (const f of [
'0000_install-eql.sql',
'0010_install-eql.sql',
'0011_install-eql.txt', // not .sql
'0001_users.sql', // doesn't match the name
]) {
writeFileSync(join(dir, f), '')
}
// Relies on drizzle-kit's zero-padded 4-digit prefix for lexical == numeric.
expect(await findGeneratedMigration(dir, 'install-eql')).toBe(
join(dir, '0010_install-eql.sql'),
)
})
})
6 changes: 3 additions & 3 deletions packages/cli/src/commands/db/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ function resolveProviderOptions(
return { supabase, drizzle, excludeOperatorFamily }
}

function printNextSteps(): void {
export function printNextSteps(): void {
p.note(
[
'Your project is set up. To encrypt your first column, pick the path',
Expand Down Expand Up @@ -887,7 +887,7 @@ async function writeSupabaseMigrationFile(
* Find the most recently generated migration file matching the given name.
* Drizzle-kit generates flat SQL files like `0000_install-eql.sql`.
*/
async function findGeneratedMigration(
export async function findGeneratedMigration(
Comment thread
coderdan marked this conversation as resolved.
outDir: string,
migrationName: string,
): Promise<string> {
Expand Down Expand Up @@ -915,7 +915,7 @@ async function findGeneratedMigration(
/**
* Attempt to clean up a generated migration file on failure.
*/
function cleanupMigrationFile(filePath: string | undefined): void {
export function cleanupMigrationFile(filePath: string | undefined): void {
if (!filePath) return

try {
Expand Down
Loading
Loading