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
41 changes: 12 additions & 29 deletions src/commands/database/database.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import { Option } from 'commander'
import inquirer from 'inquirer'
import BaseCommand from '../base-command.js'
import type { DatabaseBoilerplateType, DatabaseInitOptions } from './init.js'
import type { MigrationNewOptions } from './migration-new.js'
import type { MigrationPullOptions } from './migration-pull.js'
import type { DatabaseStatusOptions } from './status-db.js'

export type Extension = {
id: string
name: string
slug: string
hostSiteUrl: string
installedOnTeam: boolean
}

export type SiteInfo = {
id: string
name: string
account_id: string
admin_url: string
url: string
ssl_url: string
}
import type { DatabaseBoilerplateType, DatabaseInitOptions } from './legacy/db-init.js'
import type { MigrationNewOptions } from './db-migration-new.js'
import type { MigrationPullOptions } from './db-migration-pull.js'
import type { DatabaseStatusOptions } from './db-status.js'

const supportedBoilerplates = new Set<DatabaseBoilerplateType>(['drizzle'])

Expand Down Expand Up @@ -54,7 +37,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.option('--no-boilerplate', "Don't add any boilerplate to your project.")
.option('-o, --overwrite', 'Overwrites existing files that would be created when setting up boilerplate')
.action(async (_options: Record<string, unknown>, command: BaseCommand) => {
const { init } = await import('./init.js')
const { init } = await import('./legacy/db-init.js')

// Only prompt for drizzle if the user did not specify a boilerplate option, and if we're in
// interactive mode
Expand Down Expand Up @@ -85,7 +68,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.command('status')
.description(`Check the status of the database`)
.action(async (options, command) => {
const { status } = await import('./status.js')
const { status } = await import('./legacy/db-status.js')
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
await status(options, command)
})
Expand All @@ -103,7 +86,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
)
.option('--json', 'Output result as JSON')
.action(async (options: DatabaseStatusOptions, command: BaseCommand) => {
const { statusDb } = await import('./status-db.js')
const { statusDb } = await import('./db-status.js')
await statusDb(options, command)
})
.addExamples([
Expand All @@ -122,7 +105,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
'Output query results as JSON. When used without --query, prints the connection details as JSON instead.',
)
.action(async (options: { query?: string; json?: boolean }, command: BaseCommand) => {
const { connect } = await import('./connect.js')
const { connect } = await import('./db-connect.js')
await connect(options, command)
})
.addExamples([
Expand All @@ -137,7 +120,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.description('Reset the local development database, removing all data and tables')
.option('--json', 'Output result as JSON')
.action(async (options: { json?: boolean }, command: BaseCommand) => {
const { reset } = await import('./reset.js')
const { reset } = await import('./db-reset.js')
await reset(options, command)
})

Expand All @@ -149,7 +132,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.option('--to <name>', 'Target migration name or prefix to apply up to (applies all if omitted)')
.option('--json', 'Output result as JSON')
.action(async (options: { to?: string; json?: boolean }, command: BaseCommand) => {
const { migrate } = await import('./migrate.js')
const { migrate } = await import('./db-migrate.js')
await migrate(options, command)
})

Expand All @@ -165,7 +148,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
)
.option('--json', 'Output result as JSON')
.action(async (options: MigrationNewOptions, command: BaseCommand) => {
const { migrationNew } = await import('./migration-new.js')
const { migrationNew } = await import('./db-migration-new.js')
await migrationNew(options, command)
})
.addExamples([
Expand All @@ -183,7 +166,7 @@ export const createDatabaseCommand = (program: BaseCommand) => {
.option('--force', 'Skip confirmation prompt', false)
.option('--json', 'Output result as JSON')
.action(async (options: MigrationPullOptions, command: BaseCommand) => {
const { migrationPull } = await import('./migration-pull.js')
const { migrationPull } = await import('./db-migration-pull.js')
await migrationPull(options, command)
})
.addExamples([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import readline from 'readline'

import { log, logJson } from '../../utils/command-helpers.js'
import BaseCommand from '../base-command.js'
import { connectRawClient } from './db-connection.js'
import { executeMetaCommand } from './meta-commands.js'
import { formatQueryResult } from './psql-formatter.js'
import { connectRawClient } from './util/db-connection.js'
import { executeMetaCommand } from './util/meta-commands.js'
import { formatQueryResult } from './util/psql-formatter.js'

export interface ConnectOptions {
query?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { applyMigrations } from '@netlify/dev'

import { log, logJson } from '../../utils/command-helpers.js'
import BaseCommand from '../base-command.js'
import { connectToDatabase } from './db-connection.js'
import { connectToDatabase } from './util/db-connection.js'
import { DEFAULT_MIGRATIONS_PATH } from './util/migrations-path.js'

export interface MigrateOptions {
to?: string
Expand All @@ -19,7 +20,7 @@ export const migrate = async (options: MigrateOptions, command: BaseCommand) =>
const migrationsDirectory = command.netlify.config.db?.migrations?.path
if (!migrationsDirectory) {
throw new Error(
'No migrations directory found. Create a directory at netlify/db/migrations or set `db.migrations.path` in `netlify.toml`.',
`No migrations directory found. Create a directory at ${DEFAULT_MIGRATIONS_PATH} or set \`db.migrations.path\` in \`netlify.toml\`.`,
)
Comment on lines 20 to 24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Default-path guidance is misleading without actual fallback resolution.

At Line 20, the command only reads db.migrations.path from config, but Line 23 tells users to create ${DEFAULT_MIGRATIONS_PATH}. A project with only the default folder (and no config key) still errors here.

Suggested fix
-import { DEFAULT_MIGRATIONS_PATH } from './util/migrations-path.js'
+import { resolveMigrationsDirectory } from './util/migrations-path.js'
@@
-  const migrationsDirectory = command.netlify.config.db?.migrations?.path
-  if (!migrationsDirectory) {
-    throw new Error(
-      `No migrations directory found. Create a directory at ${DEFAULT_MIGRATIONS_PATH} or set \`db.migrations.path\` in \`netlify.toml\`.`,
-    )
-  }
+  const migrationsDirectory = resolveMigrationsDirectory(command)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/database/db-migrate.ts` around lines 20 - 24, The error message
is misleading because the code only uses
command.netlify.config.db?.migrations?.path (migrationsDirectory) and throws
even when the DEFAULT_MIGRATIONS_PATH exists; update the logic in db-migrate.ts
to resolve a fallback by checking the filesystem for DEFAULT_MIGRATIONS_PATH
when command.netlify.config.db?.migrations?.path is undefined or empty, set
migrationsDirectory to that default if it exists, and only throw the current
Error (mentioning DEFAULT_MIGRATIONS_PATH) if neither the configured path nor
the default folder is present; reference the migrationsDirectory variable,
DEFAULT_MIGRATIONS_PATH constant, and command.netlify.config access to locate
where to implement the filesystem existence check and fallback assignment.

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import inquirer from 'inquirer'

import { log, logJson } from '../../utils/command-helpers.js'
import BaseCommand from '../base-command.js'
import { resolveMigrationsDirectory } from './util/migrations-path.js'

export type NumberingScheme = 'sequential' | 'timestamp'

Expand Down Expand Up @@ -80,22 +81,6 @@ const getExistingMigrationNames = async (migrationsDirectory: string): Promise<s
}
}

const DEFAULT_MIGRATIONS_PATH = 'netlify/db/migrations'

export const resolveMigrationsDirectory = (command: BaseCommand): string => {
const configuredPath = command.netlify.config.db?.migrations?.path
if (configuredPath) {
return configuredPath
}

const projectRoot = command.netlify.site.root ?? command.project.root ?? command.project.baseDirectory
if (!projectRoot) {
throw new Error('Could not determine the project root directory.')
}

return join(projectRoot, DEFAULT_MIGRATIONS_PATH)
}

export const migrationNew = async (options: MigrationNewOptions, command: BaseCommand) => {
const { json } = options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import inquirer from 'inquirer'
import { log, logJson } from '../../utils/command-helpers.js'
import execa from '../../utils/execa.js'
import BaseCommand from '../base-command.js'
import { resolveMigrationsDirectory } from './migration-new.js'
import { resolveMigrationsDirectory } from './util/migrations-path.js'

export interface MigrationPullOptions {
branch?: string | true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { resetDatabase } from '@netlify/dev'

import { log, logJson } from '../../utils/command-helpers.js'
import BaseCommand from '../base-command.js'
import { connectToDatabase } from './db-connection.js'
import { connectToDatabase } from './util/db-connection.js'

export interface ResetOptions {
json?: boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
localAppliedMigrations,
type MigrationFile,
remoteAppliedMigrations,
} from './applied-migrations.js'
import { connectToDatabase, detectExistingLocalConnectionString } from './db-connection.js'
} from './util/applied-migrations.js'
import { connectToDatabase, detectExistingLocalConnectionString } from './util/db-connection.js'
import { resolveMigrationsDirectory } from './util/migrations-path.js'

export interface DatabaseStatusOptions {
branch?: string
Expand Down Expand Up @@ -38,7 +39,6 @@ interface ServerContext {
basePath: string
}

const DEFAULT_MIGRATIONS_PATH = 'netlify/db/migrations'
const DOCS_URL = 'https://ntl.fyi/database'
const NETLIFY_DATABASE_PACKAGE = '@netlify/database'

Expand All @@ -58,20 +58,6 @@ const parseVersion = (name: string): number | null => {
return Number.isFinite(parsed) ? parsed : null
}

const resolveMigrationsDirectory = (command: BaseCommand): string => {
const configuredPath = command.netlify.config.db?.migrations?.path
if (configuredPath) {
return configuredPath
}

const projectRoot = command.netlify.site.root ?? command.project.root ?? command.project.baseDirectory
if (!projectRoot) {
throw new Error('Could not determine the project root directory.')
}

return `${projectRoot}/${DEFAULT_MIGRATIONS_PATH}`
}

const readLocalMigrations = async (migrationsDirectory: string): Promise<MigrationEntry[]> => {
let entries
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const NEON_DATABASE_EXTENSION_SLUG = process.env.NEON_DATABASE_EXTENSION_SLUG ?? 'neon'
export const JIGSAW_URL = process.env.JIGSAW_URL ?? 'https://jigsaw.services-prod.nsvcs.net'
export const NETLIFY_NEON_PACKAGE_NAME = '@netlify/neon'
export const MIGRATIONS_SCHEMA = 'netlify'
export const MIGRATIONS_TABLE = `${MIGRATIONS_SCHEMA}.migrations`
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import BaseCommand from '../base-command.js'
import BaseCommand from '../../base-command.js'
import { getAccount, getExtension, getJigsawToken, getPackageJSON, installExtension, spawnAsync } from './utils.js'
import { initDrizzle } from './drizzle.js'
import { NEON_DATABASE_EXTENSION_SLUG, NETLIFY_NEON_PACKAGE_NAME } from './constants.js'
import prettyjson from 'prettyjson'
import { log } from '../../utils/command-helpers.js'
import { SiteInfo } from './database.js'
import { log } from '../../../utils/command-helpers.js'
import { SiteInfo } from './types.js'

export type DatabaseInitOptions = {
assumeNo: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { OptionValues } from 'commander'
import { SiteInfo } from './database.js'
import BaseCommand from '../base-command.js'
import { SiteInfo } from './types.js'
import BaseCommand from '../../base-command.js'
import { getAccount, getExtension, getSiteConfiguration } from './utils.js'
import { NEON_DATABASE_EXTENSION_SLUG } from './constants.js'
import prettyjson from 'prettyjson'
import { chalk, log } from '../../utils/command-helpers.js'
import { chalk, log } from '../../../utils/command-helpers.js'

export const status = async (_options: OptionValues, command: BaseCommand) => {
const siteInfo = command.netlify.siteInfo as SiteInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { carefullyWriteFile, getPackageJSON, spawnAsync } from './utils.js'
import BaseCommand from '../base-command.js'
import BaseCommand from '../../base-command.js'
import path from 'path'
import fs from 'fs/promises'
import inquirer from 'inquirer'
Expand Down
16 changes: 16 additions & 0 deletions src/commands/database/legacy/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type Extension = {
id: string
name: string
slug: string
hostSiteUrl: string
installedOnTeam: boolean
}

export type SiteInfo = {
id: string
name: string
account_id: string
admin_url: string
url: string
ssl_url: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import fs from 'fs'
import inquirer from 'inquirer'

import { JIGSAW_URL } from './constants.js'
import BaseCommand from '../base-command.js'
import { Extension } from './database.js'
import BaseCommand from '../../base-command.js'
import { Extension } from './types.js'
import { spawn } from 'child_process'

type PackageJSON = {
Expand Down
2 changes: 2 additions & 0 deletions src/commands/database/util/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const MIGRATIONS_SCHEMA = 'netlify'
export const MIGRATIONS_TABLE = `${MIGRATIONS_SCHEMA}.migrations`
24 changes: 24 additions & 0 deletions src/commands/database/util/migrations-path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { join } from 'path'

import BaseCommand from '../../base-command.js'

// Default on-disk location for migration files, relative to the project root.
// Can be overridden by setting `db.migrations.path` in `netlify.toml`.
export const DEFAULT_MIGRATIONS_PATH = 'netlify/database/migrations'

// Resolves the absolute path of the project's migrations directory. Prefers
// the `db.migrations.path` override from `netlify.toml` when present; falls
// back to `<project-root>/netlify/database/migrations`.
export const resolveMigrationsDirectory = (command: BaseCommand): string => {
const configuredPath = command.netlify.config.db?.migrations?.path
if (configuredPath) {
return configuredPath
}

const projectRoot = command.netlify.site.root ?? command.project.root ?? command.project.baseDirectory
if (!projectRoot) {
throw new Error('Could not determine the project root directory.')
}

return join(projectRoot, DEFAULT_MIGRATIONS_PATH)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ vi.mock('@netlify/dev', () => ({
applyMigrations: (...args: unknown[]) => mockApplyMigrations(...args),
}))

vi.mock('../../../../src/commands/database/db-connection.js', () => ({
vi.mock('../../../../src/commands/database/util/db-connection.js', () => ({
connectToDatabase: vi.fn().mockImplementation(() =>
Promise.resolve({
executor: mockExecutor,
Expand All @@ -33,13 +33,13 @@ vi.mock('../../../../src/utils/command-helpers.js', async () => ({
},
}))

import { migrate } from '../../../../src/commands/database/migrate.js'
import { migrate } from '../../../../src/commands/database/db-migrate.js'

function createMockCommand(overrides: { buildDir?: string; projectRoot?: string; migrationsPath?: string } = {}) {
const {
buildDir = '/project',
projectRoot = '/project',
migrationsPath = '/project/netlify/db/migrations',
migrationsPath = '/project/netlify/database/migrations',
} = overrides

return {
Expand Down
Loading
Loading