Skip to content

Commit dc0152f

Browse files
Caleb Barnesndhoulesarahetterkhendrikse
authored
feat: Add db init and db status commands (#7115)
* POC adding database init command / drizzle-kit as an external subcommand * cleanup * cleanup * fix awaiting drizzle deps installing / add status check and getSiteConfiguration * fix request headers for init endpoint / misc cleanup * only inquirer.prompt if initialOpts.drizzle is not explicitly passed as true or false * move urls to constants with env overrides / cleanup logging / remove unused function / add --yes option to use defaults and --overwrite to overwrite files * remove commented unused call to get site * remove unnecessary check * fix: change slug into the neon slug * Update db init command (#7257) * feat: add local dev branch option * add UNPOOLED env to db status command * add nf-db-user-id to req headers and update endpoint url * fix name for NETLIFY_DATABASE_URL_UNPOOLED * update NEON_DATABASE_EXTENSION_SLUG to 'neon' and remove NETLIFY_WEB_UI constant * remove dev command and dev-branch.ts * init: remove/replace headers for Nf-UIExt headers / remove dev branch questions * update getExtension, installExtension to call jigsaw directly instead of calling react ui endpoints * token -> netlifyToken * drizzle - fixes/cleanup & remove dev branch config * remove localDevBranch from Answers type --------- Co-authored-by: Karin <=> * update db command help text * update command descriptions and examples * update help command snapshot * support docs gen for sub commands without ":" * docs gen for db commands * remove "yes" flag and add "minimal" flag * improve db init ux - dont throw error on CONFLICT (db already connected) - if db already connected, we just say its connected and continue to log the status - improve status by fetching cli-db-status extension endpoint * install @netlify/neon package if not found in package.json * use same package json path * move neon package installation to end to avoid incorrect overwriting * add comment to drizzle config boilerplate for context * fix: initDrizzle - fallback to command.project.baseDirectory when command.project.root not found * perf: lazy-load `netlify db` commands * Update src/commands/database/constants.ts * feat: get user to initialize site first if they have neon installed for dev (#7319) * feat: add handling of extension requirements except doing nothing but logging * feat: require users to init a site in dev if they have neon package installed * chore: better comment * chore: format * feat: move back to logical or * refactor: address review comments --------- Co-authored-by: Nathan Houle <[email protected]> * feat: rename --minimal => --assume-no, --drizzle => --boilerplate=<type> * docs: update docs with new db flags * fix: update db init example to not use --minimal * docs: regenerate docs again --------- Co-authored-by: Karin <=> Co-authored-by: Nathan Houle <[email protected]> Co-authored-by: Sarah Etter <[email protected]> Co-authored-by: Karin Hendrikse <[email protected]>
1 parent 782de05 commit dc0152f

File tree

16 files changed

+864
-5
lines changed

16 files changed

+864
-5
lines changed

docs/commands/db.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
title: Netlify CLI db command
3+
description: Provision a production ready Postgres database with a single command
4+
sidebar:
5+
label: db
6+
---
7+
8+
# `db`
9+
10+
11+
<!-- AUTO-GENERATED-CONTENT:START (GENERATE_COMMANDS_DOCS) -->
12+
Provision a production ready Postgres database with a single command
13+
14+
**Usage**
15+
16+
```bash
17+
netlify db
18+
```
19+
20+
**Flags**
21+
22+
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
23+
- `debug` (*boolean*) - Print debugging information
24+
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
25+
26+
| Subcommand | description |
27+
|:--------------------------- |:-----|
28+
| [`init`](/commands/db#init) | Initialize a new database for the current site |
29+
| [`status`](/commands/db#status) | Check the status of the database |
30+
31+
32+
**Examples**
33+
34+
```bash
35+
netlify db status
36+
netlify db init
37+
netlify db init --help
38+
```
39+
40+
---
41+
## `init`
42+
43+
Initialize a new database for the current site
44+
45+
**Usage**
46+
47+
```bash
48+
netlify init
49+
```
50+
51+
**Flags**
52+
53+
- `assume-no` (*boolean*) - Non-interactive setup. Does not initialize any third-party tools/boilerplate. Ideal for CI environments or AI tools.
54+
- `boilerplate` (*drizzle*) - Type of boilerplate to add to your project.
55+
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
56+
- `overwrite` (*boolean*) - Overwrites existing files that would be created when setting up boilerplate
57+
- `debug` (*boolean*) - Print debugging information
58+
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
59+
- `no-boilerplate` (*boolean*) - Don't add any boilerplate to your project.
60+
61+
**Examples**
62+
63+
```bash
64+
netlify db init --assume-no
65+
netlify db init --boilerplate=drizzle --overwrite
66+
```
67+
68+
---
69+
## `status`
70+
71+
Check the status of the database
72+
73+
**Usage**
74+
75+
```bash
76+
netlify status
77+
```
78+
79+
**Flags**
80+
81+
- `debug` (*boolean*) - Print debugging information
82+
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
83+
84+
---
85+
86+
<!-- AUTO-GENERATED-CONTENT:END -->

docs/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ Generate shell completion script
5151
| [`completion:install`](/commands/completion#completioninstall) | Generates completion script for your preferred shell |
5252

5353

54+
### [db](/commands/db)
55+
56+
Provision a production ready Postgres database with a single command
57+
58+
| Subcommand | description |
59+
|:--------------------------- |:-----|
60+
| [`init`](/commands/db#init) | Initialize a new database for the current site |
61+
| [`status`](/commands/db#status) | Check the status of the database |
62+
63+
5464
### [deploy](/commands/deploy)
5565

5666
Create a new deploy from the contents of a folder

site/scripts/docs.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ const commandListSubCommandDisplay = function (commands) {
100100
let table = '| Subcommand | description |\n'
101101
table += '|:--------------------------- |:-----|\n'
102102
commands.forEach((cmd) => {
103-
const [commandBase] = cmd.name.split(':')
103+
let commandBase
104+
commandBase = cmd.name.split(':')[0]
105+
if (cmd.parent) {
106+
commandBase = cmd.parent
107+
}
104108
const baseUrl = `/commands/${commandBase}`
105109
const slug = cmd.name.replace(/:/g, '')
106110
table += `| [\`${cmd.name}\`](${baseUrl}#${slug}) | ${cmd.description.split('\n')[0]} |\n`

site/scripts/util/generate-command-data.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ const parseCommand = function (command) {
3535
}, {})
3636

3737
return {
38+
parent: command.parent?.name() !== "netlify" ? command.parent?.name() : undefined,
3839
name: command.name(),
3940
description: command.description(),
40-
commands: commands
41-
42-
.filter((cmd) => cmd.name().startsWith(`${command.name()}:`) && !cmd._hidden)
43-
.map((cmd) => parseCommand(cmd)),
41+
commands: [
42+
...command.commands.filter(cmd => !cmd._hidden).map(cmd => parseCommand(cmd)),
43+
...commands.filter((cmd) => cmd.name().startsWith(`${command.name()}:`) && !cmd._hidden).map(cmd => parseCommand(cmd))
44+
],
4445
examples: command.examples.length !== 0 && command.examples,
4546
args: args.length !== 0 && args,
4647
flags: Object.keys(flags).length !== 0 && flags,

src/commands/base-command.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { findUp } from 'find-up'
1414
import inquirer from 'inquirer'
1515
import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'
1616
import merge from 'lodash/merge.js'
17+
import pick from 'lodash/pick.js'
1718

1819
import { getAgent } from '../lib/http-agent.js'
1920
import {
@@ -785,3 +786,6 @@ export default class BaseCommand extends Command {
785786
return this.netlify.siteInfo.feature_flags?.[flagName] || null
786787
}
787788
}
789+
790+
export const getBaseOptionValues = (options: OptionValues): BaseOptionValues =>
791+
pick(options, ['auth', 'cwd', 'debug', 'filter', 'httpProxy', 'silent'])

src/commands/database/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const NEON_DATABASE_EXTENSION_SLUG = process.env.NEON_DATABASE_EXTENSION_SLUG ?? 'neon'
2+
export const JIGSAW_URL = process.env.JIGSAW_URL ?? 'https://jigsaw.services-prod.nsvcs.net'
3+
export const NETLIFY_NEON_PACKAGE_NAME = '@netlify/neon'

src/commands/database/database.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Option } from 'commander'
2+
import inquirer from 'inquirer'
3+
import BaseCommand from '../base-command.js'
4+
import type { DatabaseBoilerplateType, DatabaseInitOptions } from './init.js'
5+
6+
export type Extension = {
7+
id: string
8+
name: string
9+
slug: string
10+
hostSiteUrl: string
11+
installedOnTeam: boolean
12+
}
13+
14+
export type SiteInfo = {
15+
id: string
16+
name: string
17+
account_id: string
18+
admin_url: string
19+
url: string
20+
ssl_url: string
21+
}
22+
23+
const supportedBoilerplates = new Set<DatabaseBoilerplateType>(['drizzle'])
24+
25+
export const createDatabaseCommand = (program: BaseCommand) => {
26+
const dbCommand = program
27+
.command('db')
28+
.alias('database')
29+
.description(`Provision a production ready Postgres database with a single command`)
30+
.addExamples(['netlify db status', 'netlify db init', 'netlify db init --help'])
31+
32+
dbCommand
33+
.command('init')
34+
.description(`Initialize a new database for the current site`)
35+
.option(
36+
'--assume-no',
37+
'Non-interactive setup. Does not initialize any third-party tools/boilerplate. Ideal for CI environments or AI tools.',
38+
false,
39+
)
40+
.addOption(
41+
new Option('--boilerplate <tool>', 'Type of boilerplate to add to your project.').choices(
42+
Array.from(supportedBoilerplates).sort(),
43+
),
44+
)
45+
.option('--no-boilerplate', "Don't add any boilerplate to your project.")
46+
.option('-o, --overwrite', 'Overwrites existing files that would be created when setting up boilerplate')
47+
.action(async (_options: Record<string, unknown>, command: BaseCommand) => {
48+
const { init } = await import('./init.js')
49+
50+
// Only prompt for drizzle if the user did not specify a boilerplate option, and if we're in
51+
// interactive mode
52+
if (_options.boilerplate === undefined && !_options.assumeNo) {
53+
const answers = await inquirer.prompt<{ useDrizzle: boolean }>([
54+
{
55+
type: 'confirm',
56+
name: 'useDrizzle',
57+
message: 'Set up Drizzle boilerplate?',
58+
},
59+
])
60+
if (answers.useDrizzle) {
61+
command.setOptionValue('boilerplate', 'drizzle')
62+
}
63+
}
64+
65+
const options = _options as DatabaseInitOptions
66+
if (options.assumeNo) {
67+
options.boilerplate = false
68+
options.overwrite = false
69+
}
70+
71+
await init(options, command)
72+
})
73+
.addExamples([`netlify db init --assume-no`, `netlify db init --boilerplate=drizzle --overwrite`])
74+
75+
dbCommand
76+
.command('status')
77+
.description(`Check the status of the database`)
78+
.action(async (options, command) => {
79+
const { status } = await import('./status.js')
80+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
81+
await status(options, command)
82+
})
83+
}

src/commands/database/drizzle.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { carefullyWriteFile, getPackageJSON, spawnAsync } from './utils.js'
2+
import BaseCommand from '../base-command.js'
3+
import path from 'path'
4+
import fs from 'fs/promises'
5+
import inquirer from 'inquirer'
6+
import { NETLIFY_NEON_PACKAGE_NAME } from './constants.js'
7+
8+
export const initDrizzle = async (command: BaseCommand) => {
9+
const workingDirectory = command.project.root ?? command.project.baseDirectory
10+
if (!workingDirectory) {
11+
throw new Error('Failed to initialize Drizzle. Project root or base directory not found.')
12+
}
13+
const opts = command.opts<{
14+
overwrite?: true | undefined
15+
}>()
16+
17+
const drizzleConfigFilePath = path.resolve(workingDirectory, 'drizzle.config.ts')
18+
const schemaFilePath = path.resolve(workingDirectory, 'db/schema.ts')
19+
const dbIndexFilePath = path.resolve(workingDirectory, 'db/index.ts')
20+
if (opts.overwrite) {
21+
await fs.writeFile(drizzleConfigFilePath, drizzleConfig)
22+
await fs.mkdir(path.resolve(workingDirectory, 'db'), { recursive: true })
23+
await fs.writeFile(schemaFilePath, drizzleSchema)
24+
await fs.writeFile(dbIndexFilePath, dbIndex)
25+
} else {
26+
await carefullyWriteFile(drizzleConfigFilePath, drizzleConfig, workingDirectory)
27+
await fs.mkdir(path.resolve(workingDirectory, 'db'), { recursive: true })
28+
await carefullyWriteFile(schemaFilePath, drizzleSchema, workingDirectory)
29+
await carefullyWriteFile(dbIndexFilePath, dbIndex, workingDirectory)
30+
}
31+
32+
const packageJsonPath = path.resolve(command.workingDir, 'package.json')
33+
const packageJson = getPackageJSON(command.workingDir)
34+
35+
packageJson.scripts = {
36+
...(packageJson.scripts ?? {}),
37+
...packageJsonScripts,
38+
}
39+
if (opts.overwrite) {
40+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
41+
}
42+
43+
type Answers = {
44+
updatePackageJson: boolean
45+
}
46+
47+
if (!opts.overwrite) {
48+
const answers = await inquirer.prompt<Answers>([
49+
{
50+
type: 'confirm',
51+
name: 'updatePackageJson',
52+
message: `Add drizzle db commands to package.json?`,
53+
},
54+
])
55+
if (answers.updatePackageJson) {
56+
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
57+
}
58+
}
59+
60+
if (!Object.keys(packageJson.devDependencies ?? {}).includes('drizzle-kit')) {
61+
await spawnAsync(command.project.packageManager?.installCommand ?? 'npm install', ['drizzle-kit@latest', '-D'], {
62+
stdio: 'inherit',
63+
shell: true,
64+
})
65+
}
66+
67+
if (!Object.keys(packageJson.dependencies ?? {}).includes('drizzle-orm')) {
68+
await spawnAsync(command.project.packageManager?.installCommand ?? 'npm install', ['drizzle-orm@latest'], {
69+
stdio: 'inherit',
70+
shell: true,
71+
})
72+
}
73+
}
74+
75+
const drizzleConfig = `import { defineConfig } from 'drizzle-kit';
76+
77+
export default defineConfig({
78+
dialect: 'postgresql',
79+
dbCredentials: {
80+
url: process.env.NETLIFY_DATABASE_URL!
81+
},
82+
schema: './db/schema.ts',
83+
/**
84+
* Never edit the migrations directly, only use drizzle.
85+
* There are scripts in the package.json "db:generate" and "db:migrate" to handle this.
86+
*/
87+
out: './migrations'
88+
});`
89+
90+
const drizzleSchema = `import { integer, pgTable, varchar, text } from 'drizzle-orm/pg-core';
91+
92+
export const posts = pgTable('posts', {
93+
id: integer().primaryKey().generatedAlwaysAsIdentity(),
94+
title: varchar({ length: 255 }).notNull(),
95+
content: text().notNull().default('')
96+
});`
97+
98+
const dbIndex = `import { neon } from '${NETLIFY_NEON_PACKAGE_NAME}';
99+
import { drizzle } from 'drizzle-orm/neon-http';
100+
101+
import * as schema from './schema';
102+
103+
export const db = drizzle({
104+
schema,
105+
client: neon()
106+
});`
107+
108+
const packageJsonScripts = {
109+
'db:generate': 'drizzle-kit generate',
110+
'db:migrate': 'netlify dev:exec drizzle-kit migrate',
111+
'db:studio': 'netlify dev:exec drizzle-kit studio',
112+
}

src/commands/database/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { createDatabaseCommand } from './database.js'

0 commit comments

Comments
 (0)