Skip to content

feat: Add db init and db status commands #7115

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
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 @@ -47,6 +47,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 54 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": 54, "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
2 changes: 2 additions & 0 deletions src/commands/database/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
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'
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)
}
125 changes: 125 additions & 0 deletions src/commands/database/drizzle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { spawn } from 'child_process'
import { carefullyWriteFile } from './utils.js'
import BaseCommand from '../base-command.js'
import path from 'path'
import fs from 'fs/promises'
import inquirer from 'inquirer'

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.project.root, 'package.json')

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
packageJson.scripts = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...(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))
}
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
if (!Object.keys(packageJson?.devDependencies ?? {}).includes('drizzle-kit')) {
await spawnAsync(command.project.packageManager?.installCommand ?? 'npm install', ['drizzle-kit@latest', '-D'], {
stdio: 'inherit',
shell: true,
})
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
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',
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';
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',
}

const spawnAsync = (command: string, args: string[], options: Parameters<typeof spawn>[2]): Promise<number> => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, options)
child.on('error', reject)
child.on('exit', (code) => {
if (code === 0) {
resolve(code)
}
const errorMessage = code ? `Process exited with code ${code.toString()}` : 'Process exited with no code'
reject(new Error(errorMessage))
})
})
}
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