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
2 changes: 1 addition & 1 deletion docs/commands/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ netlify database migrations new
- `description` (*string*) - Purpose of the migration (used to generate the file name)
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `json` (*boolean*) - Output result as JSON
- `scheme` (*sequential | timestamp*) - Numbering scheme for migration prefixes
- `scheme` (*timestamp | sequential*) - Numbering scheme for migration prefixes
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

Expand Down
2 changes: 1 addition & 1 deletion src/commands/database/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.option('-d, --description <description>', 'Purpose of the migration (used to generate the file name)')
.addOption(
new Option('-s, --scheme <scheme>', 'Numbering scheme for migration prefixes').choices([
'sequential',
'timestamp',
'sequential',
]),
)
.option('--json', 'Output result as JSON')
Expand Down
57 changes: 35 additions & 22 deletions src/commands/database/db-migration-new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { log, logJson } from '../../utils/command-helpers.js'
import BaseCommand from '../base-command.js'
import { resolveMigrationsDirectory } from './util/migrations-path.js'
import { utcTimestampPrefix } from './util/timestamp.js'
import { isInteractive } from '../../utils/scripted-commands.js'

export type NumberingScheme = 'sequential' | 'timestamp'

Expand Down Expand Up @@ -89,31 +90,43 @@ export const migrationNew = async (options: MigrationNewOptions, command: BaseCo
let scheme = options.scheme

if (!description) {
const answers = await inquirer.prompt<{ description: string }>([
{
type: 'input',
name: 'description',
message: 'What is the purpose of this migration?',
validate: (input: string) => (input.trim().length > 0 ? true : 'Description cannot be empty'),
},
])
description = answers.description
if (isInteractive()) {
const answers = await inquirer.prompt<{ description: string }>([
{
type: 'input',
name: 'description',
message: 'What is the purpose of this migration?',
validate: (input: string) => (input.trim().length > 0 ? true : 'Description cannot be empty'),
},
])
description = answers.description
} else {
throw new Error(
`--description <description> argument is required when not running interactively. Provide a description of the migration (e.g. --description "add users table").`,
)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!scheme) {
const answers = await inquirer.prompt<{ scheme: NumberingScheme }>([
{
type: 'list',
name: 'scheme',
message: 'Numbering scheme:',
choices: [
{ name: 'Sequential (0001, 0002, ...)', value: 'sequential' },
{ name: 'Timestamp (20260312143000)', value: 'timestamp' },
],
...(detectedScheme && { default: detectedScheme }),
},
])
scheme = answers.scheme
const defaultScheme = detectedScheme ?? 'timestamp'

if (isInteractive()) {
const answers = await inquirer.prompt<{ scheme: NumberingScheme }>([
{
type: 'list',
name: 'scheme',
message: 'Numbering scheme:',
choices: [
{ name: 'Timestamp (e.g. 20260312143000) [Recommended]', value: 'timestamp' },
{ name: 'Sequential (e.g. 0001, 0002, ...)', value: 'sequential' },
],
default: defaultScheme,
},
])
scheme = answers.scheme
} else {
scheme = defaultScheme
}
}

const slug = generateSlug(description)
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/commands/database/db-migration-new.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ vi.mock('inquirer', () => ({
default: { prompt: vi.fn() },
}))

vi.mock('../../../../src/utils/scripted-commands.js', () => ({
isInteractive: vi.fn(),
}))

vi.mock('../../../../src/utils/command-helpers.js', async () => ({
...(await vi.importActual('../../../../src/utils/command-helpers.js')),
log: (...args: string[]) => {
Expand All @@ -46,6 +50,7 @@ import {
generateNextPrefix,
} from '../../../../src/commands/database/db-migration-new.js'
import { resolveMigrationsDirectory } from '../../../../src/commands/database/util/migrations-path.js'
import { isInteractive } from '../../../../src/utils/scripted-commands.js'

function createMockCommand(overrides: { migrationsPath?: string | undefined } = {}) {
const { migrationsPath = '/project/netlify/database/migrations' } = overrides
Expand Down Expand Up @@ -195,6 +200,7 @@ describe('migrationNew', () => {
})

test('prompts for description when not provided', async () => {
vi.mocked(isInteractive).mockReturnValue(true)
vi.mocked(inquirer.prompt)
.mockResolvedValueOnce({ description: 'create users table' })
.mockResolvedValueOnce({ scheme: 'sequential' })
Expand All @@ -205,7 +211,16 @@ describe('migrationNew', () => {
expect(mockMkdir).toHaveBeenCalledWith(expect.stringContaining('create-users-table'), expect.any(Object))
})

test('throws when description is missing in non-interactive mode', async () => {
vi.mocked(isInteractive).mockReturnValue(false)

await expect(migrationNew({}, createMockCommand())).rejects.toThrow(
'--description <description> argument is required when not running interactively',
)
})

test('prompts for scheme with detected default when not provided', async () => {
vi.mocked(isInteractive).mockReturnValue(true)
mockReaddir.mockResolvedValue([dirEntry('0001_create-users'), dirEntry('0002_add-posts')])
vi.mocked(inquirer.prompt).mockResolvedValueOnce({ scheme: 'sequential' })

Expand All @@ -215,6 +230,41 @@ describe('migrationNew', () => {
expect(promptCall[0].default).toBe('sequential')
})

test('defaults to timestamp scheme in non-interactive mode when no migrations exist', async () => {
vi.mocked(isInteractive).mockReturnValue(false)

await migrationNew({ description: 'add posts table' }, createMockCommand())

expect(inquirer.prompt).not.toHaveBeenCalled()
const mkdirCall = mockMkdir.mock.calls[0][0] as string
const folderName = mkdirCall.split(/[/\\]/).pop() ?? ''
expect(folderName).toMatch(/^\d{14}_add-posts-table$/)
})

test('detects sequential scheme in non-interactive mode when sequential migrations exist', async () => {
vi.mocked(isInteractive).mockReturnValue(false)
mockReaddir.mockResolvedValue([dirEntry('0001_create-users'), dirEntry('0002_add-posts')])

await migrationNew({ description: 'add comments' }, createMockCommand())

expect(inquirer.prompt).not.toHaveBeenCalled()
expect(mockMkdir).toHaveBeenCalledWith(join('/project/netlify/database/migrations', '0003_add-comments'), {
recursive: true,
})
})

test('detects timestamp scheme in non-interactive mode when timestamp migrations exist', async () => {
vi.mocked(isInteractive).mockReturnValue(false)
mockReaddir.mockResolvedValue([dirEntry('20260312143000_create-users'), dirEntry('20260312150000_add-posts')])

await migrationNew({ description: 'add comments' }, createMockCommand())

expect(inquirer.prompt).not.toHaveBeenCalled()
const mkdirCall = mockMkdir.mock.calls[0][0] as string
const folderName = mkdirCall.split(/[/\\]/).pop() ?? ''
expect(folderName).toMatch(/^\d{14}_add-comments$/)
})

test('uses timestamp scheme when specified', async () => {
await migrationNew({ description: 'add posts table', scheme: 'timestamp' }, createMockCommand())

Expand Down
Loading