Skip to content

feat: Add db init and db status commands #7115

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a33288e
POC adding database init command / drizzle-kit as an external subcommand
CalebBarnes Mar 20, 2025
fffd673
cleanup
CalebBarnes Mar 20, 2025
4f44728
cleanup
CalebBarnes Mar 24, 2025
759fccb
fix awaiting drizzle deps installing / add status check and getSiteCo…
CalebBarnes Mar 24, 2025
dd1699d
fix request headers for init endpoint / misc cleanup
CalebBarnes Mar 25, 2025
107faf7
only inquirer.prompt if initialOpts.drizzle is not explicitly passed …
CalebBarnes Mar 25, 2025
2fd7ca0
move urls to constants with env overrides / cleanup logging / remove …
CalebBarnes Mar 25, 2025
b9e566d
remove commented unused call to get site
CalebBarnes Mar 25, 2025
5b08ee3
remove unnecessary check
CalebBarnes Mar 25, 2025
115e19f
fix: change slug into the neon slug
May 1, 2025
c4155cd
Update db init command (#7257)
CalebBarnes May 2, 2025
c0459ac
update db command help text
CalebBarnes May 2, 2025
f30f195
update command descriptions and examples
CalebBarnes May 2, 2025
836fd29
update help command snapshot
CalebBarnes May 2, 2025
254cc6d
support docs gen for sub commands without ":"
CalebBarnes May 2, 2025
9085a8f
docs gen for db commands
CalebBarnes May 2, 2025
dbb78c7
remove "yes" flag and add "minimal" flag
CalebBarnes May 2, 2025
e151484
improve db init ux - dont throw error on CONFLICT (db already connected)
CalebBarnes May 5, 2025
cadddbc
install @netlify/neon package if not found in package.json
CalebBarnes May 14, 2025
6ba266a
use same package json path
CalebBarnes May 20, 2025
a2d9483
move neon package installation to end to avoid incorrect overwriting
CalebBarnes May 20, 2025
255457f
add comment to drizzle config boilerplate for context
CalebBarnes May 20, 2025
1dd6961
Merge branch 'main' into feat/netlify-database-command
CalebBarnes May 21, 2025
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
86 changes: 86 additions & 0 deletions docs/commands/db.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
title: Netlify CLI db command
description: Provision a production ready Postgres database with a single command
sidebar:
label: db
---

# `db`


<!-- AUTO-GENERATED-CONTENT:START (GENERATE_COMMANDS_DOCS) -->
Provision a production ready Postgres database with a single command

**Usage**

```bash
netlify db
```

**Flags**

- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

| Subcommand | description |

Check warning on line 26 in docs/commands/db.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'Subcommand'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'Subcommand'?", "location": {"path": "docs/commands/db.md", "range": {"start": {"line": 26, "column": 3}}}, "severity": "WARNING"}
|:--------------------------- |:-----|
| [`init`](/commands/db#init) | Initialize a new database for the current site |
| [`status`](/commands/db#status) | Check the status of the database |


**Examples**

```bash
netlify db status
netlify db init
netlify db init --help
```

---
## `init`

Initialize a new database for the current site

**Usage**

```bash
netlify init
```

**Flags**

- `drizzle` (*boolean*) - Initialize basic drizzle config and schema boilerplate

Check warning on line 53 in docs/commands/db.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'config'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'config'?", "location": {"path": "docs/commands/db.md", "range": {"start": {"line": 53, "column": 52}}}, "severity": "WARNING"}
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `minimal` (*boolean*) - Minimal non-interactive setup. Does not initialize drizzle or any boilerplate. Ideal for CI or AI tools.
- `no-drizzle` (*boolean*) - Does not initialize drizzle and skips any related prompts
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `overwrite` (*boolean*) - Overwrites existing files that would be created when setting up drizzle

**Examples**

```bash
netlify db init --minimal
netlify db init --drizzle --overwrite
```

---
## `status`

Check the status of the database

**Usage**

```bash
netlify status
```

**Flags**

- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

---

<!-- AUTO-GENERATED-CONTENT:END -->
10 changes: 10 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@
| [`completion:install`](/commands/completion#completioninstall) | Generates completion script for your preferred shell |


### [db](/commands/db)

Provision a production ready Postgres database with a single command

| Subcommand | description |

Check warning on line 58 in docs/index.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'Subcommand'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'Subcommand'?", "location": {"path": "docs/index.md", "range": {"start": {"line": 58, "column": 3}}}, "severity": "WARNING"}
|:--------------------------- |:-----|
| [`init`](/commands/db#init) | Initialize a new database for the current site |
| [`status`](/commands/db#status) | Check the status of the database |


### [deploy](/commands/deploy)

Create a new deploy from the contents of a folder
Expand Down
6 changes: 5 additions & 1 deletion site/scripts/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ const commandListSubCommandDisplay = function (commands) {
let table = '| Subcommand | description |\n'
table += '|:--------------------------- |:-----|\n'
commands.forEach((cmd) => {
const [commandBase] = cmd.name.split(':')
let commandBase
commandBase = cmd.name.split(':')[0]
if (cmd.parent) {
commandBase = cmd.parent
}
const baseUrl = `/commands/${commandBase}`
const slug = cmd.name.replace(/:/g, '')
table += `| [\`${cmd.name}\`](${baseUrl}#${slug}) | ${cmd.description.split('\n')[0]} |\n`
Expand Down
9 changes: 5 additions & 4 deletions site/scripts/util/generate-command-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ const parseCommand = function (command) {
}, {})

return {
parent: command.parent?.name() !== "netlify" ? command.parent?.name() : undefined,
name: command.name(),
description: command.description(),
commands: commands

.filter((cmd) => cmd.name().startsWith(`${command.name()}:`) && !cmd._hidden)
.map((cmd) => parseCommand(cmd)),
commands: [
...command.commands.filter(cmd => !cmd._hidden).map(cmd => parseCommand(cmd)),
...commands.filter((cmd) => cmd.name().startsWith(`${command.name()}:`) && !cmd._hidden).map(cmd => parseCommand(cmd))
],
examples: command.examples.length !== 0 && command.examples,
args: args.length !== 0 && args,
flags: Object.keys(flags).length !== 0 && flags,
Expand Down
3 changes: 3 additions & 0 deletions src/commands/database/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const NEON_DATABASE_EXTENSION_SLUG = process.env.NEON_DATABASE_EXTENSION_SLUG ?? 'neon'
export const JIGSAW_URL = process.env.JIGSAW_URL ?? 'https://api.netlifysdk.com'
export const NETLIFY_NEON_PACKAGE_NAME = '@netlify/neon'
43 changes: 43 additions & 0 deletions src/commands/database/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import BaseCommand from '../base-command.js'
import { status } from './status.js'
import { init } from './init.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
}

export const createDatabaseCommand = (program: BaseCommand) => {
const dbCommand = program
.command('db')
.alias('database')
.description(`Provision a production ready Postgres database with a single command`)
.addExamples(['netlify db status', 'netlify db init', 'netlify db init --help'])

dbCommand
.command('init')
.description(`Initialize a new database for the current site`)
.option(`--drizzle`, 'Initialize basic drizzle config and schema boilerplate')
.option('--no-drizzle', 'Does not initialize drizzle and skips any related prompts')
.option(
'--minimal',
'Minimal non-interactive setup. Does not initialize drizzle or any boilerplate. Ideal for CI or AI tools.',
)
.option('-o, --overwrite', 'Overwrites existing files that would be created when setting up drizzle')
.action(init)
.addExamples([`netlify db init --minimal`, `netlify db init --drizzle --overwrite`])

dbCommand.command('status').description(`Check the status of the database`).action(status)
}
111 changes: 111 additions & 0 deletions src/commands/database/drizzle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { carefullyWriteFile, getPackageJSON, spawnAsync } from './utils.js'
import BaseCommand from '../base-command.js'
import path from 'path'
import fs from 'fs/promises'
import inquirer from 'inquirer'
import { NETLIFY_NEON_PACKAGE_NAME } from './constants.js'

export const initDrizzle = async (command: BaseCommand) => {
if (!command.project.root) {
throw new Error('Failed to initialize Drizzle in project. Project root not found.')
}
const opts = command.opts<{
overwrite?: true | undefined
}>()

const drizzleConfigFilePath = path.resolve(command.project.root, 'drizzle.config.ts')
const schemaFilePath = path.resolve(command.project.root, 'db/schema.ts')
const dbIndexFilePath = path.resolve(command.project.root, 'db/index.ts')
if (opts.overwrite) {
await fs.writeFile(drizzleConfigFilePath, drizzleConfig)
await fs.mkdir(path.resolve(command.project.root, 'db'), { recursive: true })
await fs.writeFile(schemaFilePath, drizzleSchema)
await fs.writeFile(dbIndexFilePath, dbIndex)
} else {
await carefullyWriteFile(drizzleConfigFilePath, drizzleConfig, command.project.root)
await fs.mkdir(path.resolve(command.project.root, 'db'), { recursive: true })
await carefullyWriteFile(schemaFilePath, drizzleSchema, command.project.root)
await carefullyWriteFile(dbIndexFilePath, dbIndex, command.project.root)
}

const packageJsonPath = path.resolve(command.workingDir, 'package.json')
const packageJson = getPackageJSON(command.workingDir)

packageJson.scripts = {
...(packageJson.scripts ?? {}),
...packageJsonScripts,
}
if (opts.overwrite) {
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
}

type Answers = {
updatePackageJson: boolean
}

if (!opts.overwrite) {
const answers = await inquirer.prompt<Answers>([
{
type: 'confirm',
name: 'updatePackageJson',
message: `Add drizzle db commands to package.json?`,
},
])
if (answers.updatePackageJson) {
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
}
}

if (!Object.keys(packageJson.devDependencies ?? {}).includes('drizzle-kit')) {
await spawnAsync(command.project.packageManager?.installCommand ?? 'npm install', ['drizzle-kit@latest', '-D'], {
stdio: 'inherit',
shell: true,
})
}

if (!Object.keys(packageJson.dependencies ?? {}).includes('drizzle-orm')) {
await spawnAsync(command.project.packageManager?.installCommand ?? 'npm install', ['drizzle-orm@latest'], {
stdio: 'inherit',
shell: true,
})
}
}

const drizzleConfig = `import { defineConfig } from 'drizzle-kit';

export default defineConfig({
dialect: 'postgresql',
dbCredentials: {
url: process.env.NETLIFY_DATABASE_URL!
},
schema: './db/schema.ts',
/**
* Never edit the migrations directly, only use drizzle.
* There are scripts in the package.json "db:generate" and "db:migrate" to handle this.
*/
out: './migrations'
});`

const drizzleSchema = `import { integer, pgTable, varchar, text } from 'drizzle-orm/pg-core';

export const posts = pgTable('posts', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
title: varchar({ length: 255 }).notNull(),
content: text().notNull().default('')
});`

const dbIndex = `import { neon } from '${NETLIFY_NEON_PACKAGE_NAME}';
import { drizzle } from 'drizzle-orm/neon-http';

import * as schema from './schema';

export const db = drizzle({
schema,
client: neon()
});`

const packageJsonScripts = {
'db:generate': 'drizzle-kit generate',
'db:migrate': 'netlify dev:exec drizzle-kit migrate',
'db:studio': 'netlify dev:exec drizzle-kit studio',
}
1 change: 1 addition & 0 deletions src/commands/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createDatabaseCommand } from './database.js'
Loading
Loading