Skip to content

Commit 1cf19cb

Browse files
committed
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 <=>
1 parent c53fad4 commit 1cf19cb

File tree

6 files changed

+390
-271
lines changed

6 files changed

+390
-271
lines changed

src/commands/database/constants.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export const NEON_DATABASE_EXTENSION_SLUG = process.env.NEON_DATABASE_EXTENSION_SLUG ?? 'neon'
22
export const JIGSAW_URL = process.env.JIGSAW_URL ?? 'https://api.netlifysdk.com'
3-
export const NETLIFY_WEB_UI = process.env.NETLIFY_WEB_UI ?? 'https://app.netlify.com'

src/commands/database/database.ts

+12-220
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { OptionValues } from 'commander'
2-
import inquirer from 'inquirer'
31
import BaseCommand from '../base-command.js'
4-
import { getAccount, getExtension, getSiteConfiguration, installExtension } from './utils.js'
5-
import { initDrizzle } from './drizzle.js'
6-
import { NEON_DATABASE_EXTENSION_SLUG } from './constants.js'
7-
import prettyjson from 'prettyjson'
8-
import { chalk, log } from '../../utils/command-helpers.js'
2+
import { status } from './status.js'
3+
import { init } from './init.js'
94

10-
type SiteInfo = {
5+
export type Extension = {
6+
id: string
7+
name: string
8+
slug: string
9+
hostSiteUrl: string
10+
installedOnTeam: boolean
11+
}
12+
13+
export type SiteInfo = {
1114
id: string
1215
name: string
1316
account_id: string
@@ -16,224 +19,13 @@ type SiteInfo = {
1619
ssl_url: string
1720
}
1821

19-
const init = async (_options: OptionValues, command: BaseCommand) => {
20-
const siteInfo = command.netlify.siteInfo as SiteInfo
21-
if (!command.siteId) {
22-
console.error(`The project must be linked with netlify link before initializing a database.`)
23-
return
24-
}
25-
26-
const initialOpts = command.opts()
27-
28-
/**
29-
* Only prompt for drizzle if the user did not pass in the `--drizzle` or `--no-drizzle` option
30-
*/
31-
if (initialOpts.drizzle !== false && initialOpts.drizzle !== true && !initialOpts.yes) {
32-
type Answers = {
33-
drizzle: boolean
34-
}
35-
const answers = await inquirer.prompt<Answers>([
36-
{
37-
type: 'confirm',
38-
name: 'drizzle',
39-
message: 'Use Drizzle?',
40-
},
41-
])
42-
command.setOptionValue('drizzle', answers.drizzle)
43-
}
44-
45-
const opts = command.opts<{
46-
drizzle?: boolean | undefined
47-
/**
48-
* Skip prompts and use default values (answer yes to all prompts)
49-
*/
50-
yes?: true | undefined
51-
}>()
52-
53-
if (opts.drizzle || (opts.yes && opts.drizzle !== false)) {
54-
await initDrizzle(command)
55-
}
56-
57-
if (!command.netlify.api.accessToken) {
58-
throw new Error(`Please login with netlify login before running this command`)
59-
}
60-
61-
if (!siteInfo.account_id || !siteInfo.name) {
62-
throw new Error(`Error getting site, make sure you are logged in with netlify login`)
63-
}
64-
65-
const account = await getAccount(command, { accountId: siteInfo.account_id })
66-
67-
log(`Initializing a new database...`)
68-
69-
const netlifyToken = command.netlify.api.accessToken.replace('Bearer ', '')
70-
const extension = await getExtension({
71-
accountId: siteInfo.account_id,
72-
token: netlifyToken,
73-
slug: NEON_DATABASE_EXTENSION_SLUG,
74-
})
75-
if (!extension?.hostSiteUrl) {
76-
throw new Error(`Failed to get extension host site url when installing extension`)
77-
}
78-
79-
const installNeonExtension = async () => {
80-
if (!siteInfo.account_id || !account.name) {
81-
throw new Error(`Failed to install extension "${extension.name}"`)
82-
}
83-
const installed = await installExtension({
84-
accountId: siteInfo.account_id,
85-
token: netlifyToken,
86-
slug: NEON_DATABASE_EXTENSION_SLUG,
87-
hostSiteUrl: extension.hostSiteUrl,
88-
})
89-
if (!installed) {
90-
throw new Error(`Failed to install extension on team "${account.name}": "${extension.name}"`)
91-
}
92-
log(`Extension "${extension.name}" successfully installed on team "${account.name}"`)
93-
}
94-
95-
if (!extension.installedOnTeam && !opts.yes) {
96-
type Answers = {
97-
installExtension: boolean
98-
}
99-
const answers = await inquirer.prompt<Answers>([
100-
{
101-
type: 'confirm',
102-
name: 'installExtension',
103-
message: `The required extension "${extension.name}" is not installed on team "${account.name}", would you like to install it now?`,
104-
},
105-
])
106-
if (answers.installExtension) {
107-
await installNeonExtension()
108-
} else {
109-
return
110-
}
111-
}
112-
if (!extension.installedOnTeam && opts.yes) {
113-
await installNeonExtension()
114-
}
115-
116-
try {
117-
const siteEnv = await command.netlify.api.getEnvVar({
118-
accountId: siteInfo.account_id,
119-
siteId: command.siteId,
120-
key: 'NETLIFY_DATABASE_URL',
121-
})
122-
123-
if (siteEnv.key === 'NETLIFY_DATABASE_URL') {
124-
log(`Environment variable "NETLIFY_DATABASE_URL" already exists on site: ${siteInfo.name}`)
125-
log(`You can run "netlify db status" to check the status for this site`)
126-
return
127-
}
128-
} catch {
129-
// no op, env var does not exist, so we just continue
130-
}
131-
132-
const hostSiteUrl = process.env.NEON_DATABASE_EXTENSION_HOST_SITE_URL ?? extension.hostSiteUrl
133-
const initEndpoint = new URL('/cli-db-init', hostSiteUrl).toString()
134-
135-
const req = await fetch(initEndpoint, {
136-
method: 'POST',
137-
headers: {
138-
'Content-Type': 'application/json',
139-
'nf-db-token': netlifyToken,
140-
'nf-db-site-id': command.siteId,
141-
'nf-db-account-id': siteInfo.account_id,
142-
},
143-
})
144-
145-
if (!req.ok) {
146-
throw new Error(`Failed to initialize DB: ${await req.text()}`)
147-
}
148-
149-
const res = (await req.json()) as {
150-
code?: string
151-
message?: string
152-
}
153-
if (res.code === 'DATABASE_INITIALIZED') {
154-
if (res.message) {
155-
log(res.message)
156-
}
157-
158-
log(
159-
prettyjson.render({
160-
'Current team': account.name,
161-
'Current site': siteInfo.name,
162-
[`${extension.name} extension`]: 'installed',
163-
Database: 'connected',
164-
'Site environment variable': 'NETLIFY_DATABASE_URL',
165-
}),
166-
)
167-
}
168-
return
169-
}
170-
171-
const status = async (_options: OptionValues, command: BaseCommand) => {
172-
const siteInfo = command.netlify.siteInfo as SiteInfo
173-
if (!command.siteId) {
174-
throw new Error(`The project must be linked with netlify link before initializing a database.`)
175-
}
176-
if (!siteInfo.account_id) {
177-
throw new Error(`No account id found for site ${command.siteId}`)
178-
}
179-
if (!command.netlify.api.accessToken) {
180-
throw new Error(`You must be logged in with netlify login to check the status of the database`)
181-
}
182-
const netlifyToken = command.netlify.api.accessToken.replace('Bearer ', '')
183-
184-
const account = await getAccount(command, { accountId: siteInfo.account_id })
185-
186-
let siteEnv: Awaited<ReturnType<typeof command.netlify.api.getEnvVar>> | undefined
187-
try {
188-
siteEnv = await command.netlify.api.getEnvVar({
189-
accountId: siteInfo.account_id,
190-
siteId: command.siteId,
191-
key: 'NETLIFY_DATABASE_URL',
192-
})
193-
} catch {
194-
// no-op, env var does not exist, so we just continue
195-
}
196-
197-
const extension = await getExtension({
198-
accountId: account.id,
199-
token: netlifyToken,
200-
slug: NEON_DATABASE_EXTENSION_SLUG,
201-
})
202-
let siteConfig
203-
try {
204-
siteConfig = await getSiteConfiguration({
205-
siteId: command.siteId,
206-
accountId: siteInfo.account_id,
207-
slug: NEON_DATABASE_EXTENSION_SLUG,
208-
token: netlifyToken,
209-
})
210-
} catch {
211-
// no-op, site config does not exist or extension not installed
212-
}
213-
214-
log(
215-
prettyjson.render({
216-
'Current team': account.name,
217-
'Current site': siteInfo.name,
218-
[extension?.name ? `${extension.name} extension` : 'Database extension']: extension?.installedOnTeam
219-
? 'installed'
220-
: chalk.red('not installed'),
221-
// @ts-expect-error -- siteConfig is not typed
222-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
223-
Database: siteConfig?.config?.neonProjectId ? 'connected' : chalk.red('not connected'),
224-
'Site environment variable':
225-
siteEnv?.key === 'NETLIFY_DATABASE_URL' ? 'NETLIFY_DATABASE_URL' : chalk.red('NETLIFY_DATABASE_URL not set'),
226-
}),
227-
)
228-
}
229-
23022
export const createDatabaseCommand = (program: BaseCommand) => {
23123
const dbCommand = program.command('db').alias('database').description(`TODO: write description for database command`)
23224

23325
dbCommand
23426
.command('init')
23527
.description('Initialize a new database')
236-
.option('--drizzle', 'Sets up drizzle-kit and drizzle-orm in your project')
28+
.option(`--drizzle`, 'Initialize basic drizzle config and schema boilerplate')
23729
.option('--no-drizzle', 'Skips drizzle')
23830
.option('-y, --yes', 'Skip prompts and use default values')
23931
.option('-o, --overwrite', 'Overwrites existing files that would be created when setting up drizzle')

src/commands/database/drizzle.ts

+15-14
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ export const initDrizzle = async (command: BaseCommand) => {
1212
const opts = command.opts<{
1313
overwrite?: true | undefined
1414
}>()
15+
1516
const drizzleConfigFilePath = path.resolve(command.project.root, 'drizzle.config.ts')
1617
const schemaFilePath = path.resolve(command.project.root, 'db/schema.ts')
1718
const dbIndexFilePath = path.resolve(command.project.root, 'db/index.ts')
1819
if (opts.overwrite) {
1920
await fs.writeFile(drizzleConfigFilePath, drizzleConfig)
2021
await fs.mkdir(path.resolve(command.project.root, 'db'), { recursive: true })
21-
await fs.writeFile(schemaFilePath, exampleDrizzleSchema)
22-
await fs.writeFile(dbIndexFilePath, exampleDbIndex)
22+
await fs.writeFile(schemaFilePath, drizzleSchema)
23+
await fs.writeFile(dbIndexFilePath, dbIndex)
2324
} else {
2425
await carefullyWriteFile(drizzleConfigFilePath, drizzleConfig, command.project.root)
2526
await fs.mkdir(path.resolve(command.project.root, 'db'), { recursive: true })
26-
await carefullyWriteFile(schemaFilePath, exampleDrizzleSchema, command.project.root)
27-
await carefullyWriteFile(dbIndexFilePath, exampleDbIndex, command.project.root)
27+
await carefullyWriteFile(schemaFilePath, drizzleSchema, command.project.root)
28+
await carefullyWriteFile(dbIndexFilePath, dbIndex, command.project.root)
2829
}
2930

3031
const packageJsonPath = path.resolve(command.project.root, 'package.json')
@@ -41,10 +42,11 @@ export const initDrizzle = async (command: BaseCommand) => {
4142
await fs.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))
4243
}
4344

45+
type Answers = {
46+
updatePackageJson: boolean
47+
}
48+
4449
if (!opts.overwrite) {
45-
type Answers = {
46-
updatePackageJson: boolean
47-
}
4850
const answers = await inquirer.prompt<Answers>([
4951
{
5052
type: 'confirm',
@@ -84,29 +86,28 @@ export default defineConfig({
8486
out: './migrations'
8587
});`
8688

87-
const exampleDrizzleSchema = `import { integer, pgTable, varchar, text } from 'drizzle-orm/pg-core';
89+
const drizzleSchema = `import { integer, pgTable, varchar, text } from 'drizzle-orm/pg-core';
8890
8991
export const posts = pgTable('posts', {
9092
id: integer().primaryKey().generatedAlwaysAsIdentity(),
9193
title: varchar({ length: 255 }).notNull(),
9294
content: text().notNull().default('')
9395
});`
9496

95-
const exampleDbIndex = `import { neon } from '@netlify/neon';
97+
const dbIndex = `import { neon } from '@netlify/neon';
9698
import { drizzle } from 'drizzle-orm/neon-http';
9799
98-
import * as schema from 'db/schema';
100+
import * as schema from './schema';
99101
100102
export const db = drizzle({
101103
schema,
102104
client: neon()
103105
});`
104106

105107
const packageJsonScripts = {
106-
'db:generate': 'netlify dev:exec --context dev drizzle-kit generate',
107-
'db:migrate': 'netlify dev:exec --context dev drizzle-kit migrate',
108-
'db:studio': 'netlify dev:exec --context dev drizzle-kit studio',
109-
'db:push': 'netlify dev:exec --context dev drizzle-kit push',
108+
'db:generate': 'drizzle-kit generate',
109+
'db:migrate': 'netlify dev:exec drizzle-kit migrate',
110+
'db:studio': 'netlify dev:exec drizzle-kit studio',
110111
}
111112

112113
const spawnAsync = (command: string, args: string[], options: Parameters<typeof spawn>[2]): Promise<number> => {

0 commit comments

Comments
 (0)