-
Notifications
You must be signed in to change notification settings - Fork 6
fix(cli): validate --name and pass --out in the v2 Drizzle migration generator #703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
46dde37
fix(cli): validate --name and pass --out in the v2 Drizzle migration …
tobyhede 39bd706
test(cli): cover the CliExit re-throw and pin the drizzle-kit argv
tobyhede 8a6a0a7
test(cli): cover v2 drizzle generator failure paths; document dlx vs …
tobyhede 62b41d1
fix(cli): run v2 drizzle-kit project-locally (execArgv), matching v3
tobyhede File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| 'stash': patch | ||
| --- | ||
|
|
||
| Fix two defects in the Drizzle migration generator used by `stash eql install --drizzle` (EQL v2): | ||
|
|
||
| - **`--name` is now validated and no longer reaches a shell.** The migration name was interpolated into a shell command string, so a name containing shell metacharacters (e.g. `--name 'x; rm -rf ~'`) was executed. `--name` is now restricted to letters, numbers, dashes, and underscores, and drizzle-kit is invoked with an argv array instead of a shell string. | ||
| - **`--out` is now actually passed to drizzle-kit.** The flag was used to search for the generated migration but never handed to `drizzle-kit generate`, so any project whose `drizzle.config.ts` writes migrations outside `drizzle/` had the file written in one place and searched for in another, failing with "migration file not found". | ||
|
|
||
| `stash eql migration --drizzle` (EQL v3) already had both fixes and is unchanged. |
146 changes: 146 additions & 0 deletions
146
packages/cli/src/commands/db/__tests__/generate-drizzle-migration.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| import { | ||
| mkdirSync, | ||
| mkdtempSync, | ||
| readFileSync, | ||
| rmSync, | ||
| writeFileSync, | ||
| } from 'node:fs' | ||
| import { tmpdir } from 'node:os' | ||
| import { join } from 'node:path' | ||
| import * as p from '@clack/prompts' | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' | ||
| import { CliExit } from '../../../cli/exit.js' | ||
| import { messages } from '../../../messages.js' | ||
|
|
||
| // clack is chrome — silence it and spy on the channels the generator reports | ||
| // through. The spinner instance doubles as the `s` argument. | ||
| const clack = vi.hoisted(() => ({ | ||
| spinnerInstance: { start: vi.fn(), stop: vi.fn() }, | ||
| log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), success: vi.fn() }, | ||
| intro: vi.fn(), | ||
| note: vi.fn(), | ||
| outro: vi.fn(), | ||
| })) | ||
| vi.mock('@clack/prompts', () => ({ | ||
| spinner: vi.fn(() => clack.spinnerInstance), | ||
| log: clack.log, | ||
| intro: clack.intro, | ||
| note: clack.note, | ||
| outro: clack.outro, | ||
| })) | ||
|
|
||
| // Only the child process is faked — everything else (fs, bundled SQL) is real. | ||
| const spawnMock = vi.hoisted(() => vi.fn()) | ||
| vi.mock('node:child_process', () => ({ spawnSync: spawnMock })) | ||
|
|
||
| // Pin the package manager so the argv assertion below is exact. Detection reads | ||
| // the lockfile in cwd and npm_config_user_agent, both of which vary by how the | ||
| // suite was launched. The runner MAPPING stays real — `pnpm` + `['dlx', …]` is | ||
| // part of what's being asserted, so mocking it would defeat the test. | ||
| vi.mock('@/commands/init/utils.js', async (importOriginal) => ({ | ||
| ...(await importOriginal<typeof import('@/commands/init/utils.js')>()), | ||
| detectPackageManager: () => 'pnpm', | ||
| })) | ||
|
|
||
| const { generateDrizzleMigration } = await import('../install.js') | ||
|
|
||
| const spinner = p.spinner() | ||
|
|
||
| afterEach(() => { | ||
| vi.clearAllMocks() | ||
| }) | ||
|
|
||
| /** | ||
| * The v2 (`eql install --drizzle`) generator. Both regressions pinned here are | ||
| * invocation-level: an unvalidated `--name` reaching a shell string, and | ||
| * `--out` being computed for the search but never handed to drizzle-kit. | ||
| */ | ||
| describe('generateDrizzleMigration', () => { | ||
| let tmp: string | ||
| beforeEach(() => { | ||
| tmp = mkdtempSync(join(tmpdir(), 'stash-v2-drizzle-migration-')) | ||
| }) | ||
| afterEach(() => { | ||
| rmSync(tmp, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| it('rejects a migration name with unsafe characters before spawning', async () => { | ||
| await expect( | ||
| generateDrizzleMigration(spinner, { | ||
| name: 'x; rm -rf ~', | ||
| out: join(tmp, 'drizzle'), | ||
| }), | ||
| ).rejects.toBeInstanceOf(CliExit) | ||
| expect(clack.log.error).toHaveBeenCalledWith(messages.eql.migrationBadName) | ||
| expect(spawnMock).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it.each([ | ||
| ['command substitution', 'a$(whoami)'], | ||
| ['backticks', 'a`id`'], | ||
| ['a space', 'add eql'], | ||
| ['a path separator', '../escape'], | ||
| ])('rejects %s in --name', async (_label, name) => { | ||
| await expect( | ||
| generateDrizzleMigration(spinner, { name, out: join(tmp, 'drizzle') }), | ||
| ).rejects.toBeInstanceOf(CliExit) | ||
| expect(spawnMock).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('rejects an unsafe name in a dry run too (validation precedes the preview)', async () => { | ||
| await expect( | ||
| generateDrizzleMigration(spinner, { name: 'x; ls', dryRun: true }), | ||
| ).rejects.toBeInstanceOf(CliExit) | ||
| expect(clack.note).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| it('passes --name and --out to drizzle-kit as argv (no shell) and writes the SQL', async () => { | ||
| const out = join(tmp, 'db', 'migrations') | ||
| mkdirSync(out, { recursive: true }) | ||
| // Stand in for drizzle-kit scaffolding an empty custom migration. | ||
| spawnMock.mockImplementation(() => { | ||
| writeFileSync(join(out, '0000_add-eql.sql'), '') | ||
| return { status: 0, stdout: '', stderr: '' } | ||
| }) | ||
|
|
||
| await generateDrizzleMigration(spinner, { name: 'add-eql', out }) | ||
|
|
||
| expect(spawnMock).toHaveBeenCalledTimes(1) | ||
| const [command, argv] = spawnMock.mock.calls[0] | ||
| // The whole argv, exactly — not `toContain` checks, which would still pass | ||
| // if the runner prefix (`dlx`) were dropped and drizzle-kit ran under the | ||
| // wrong resolver. DEFECT 1: name and out are discrete inert tokens in an | ||
| // array, never interpolated into a shell string. DEFECT 2: `--out` is | ||
| // actually passed, so drizzle-kit writes where step 2 then looks. | ||
| expect(command).toBe('pnpm') | ||
| expect(argv).toEqual([ | ||
| 'dlx', | ||
| 'drizzle-kit', | ||
| 'generate', | ||
| '--custom', | ||
| '--name=add-eql', | ||
| `--out=${out}`, | ||
| ]) | ||
|
|
||
| const written = readFileSync(join(out, '0000_add-eql.sql'), 'utf-8') | ||
| expect(written).toContain('cs_migrations') | ||
| }) | ||
|
|
||
| it('includes --out in the dry-run preview', async () => { | ||
| const out = join(tmp, 'custom-out') | ||
| await generateDrizzleMigration(spinner, { dryRun: true, out }) | ||
| expect(spawnMock).not.toHaveBeenCalled() | ||
| expect(clack.note).toHaveBeenCalledWith( | ||
| expect.stringContaining(`--out=${out}`), | ||
| 'Dry Run', | ||
| ) | ||
| }) | ||
|
|
||
| it('aborts with CliExit when drizzle-kit exits non-zero', async () => { | ||
| spawnMock.mockReturnValue({ status: 1, stdout: '', stderr: 'boom' }) | ||
| await expect( | ||
| generateDrizzleMigration(spinner, { out: join(tmp, 'drizzle') }), | ||
| ).rejects.toBeInstanceOf(CliExit) | ||
| expect(clack.log.error).toHaveBeenCalledWith('boom') | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.