Init.ts•22.4 kB
import fs from 'node:fs'
import path from 'node:path'
import { confirm, input, select } from '@inquirer/prompts'
import { PrismaConfigInternal } from '@prisma/config'
import { unstable_startServer } from '@prisma/dev'
import { ServerState } from '@prisma/dev/internal/state'
import type { ConnectorType } from '@prisma/generator'
import {
arg,
canConnectToDatabase,
checkUnsupportedDataProxy,
Command,
format,
getCommandWithExecutor,
HelpError,
isError,
link,
logger,
PRISMA_POSTGRES_PROVIDER,
protocolToConnectorType,
} from '@prisma/internals'
import dotenv from 'dotenv'
import { Schema as Shape } from 'effect'
import { bold, dim, green, red, yellow } from 'kleur/colors'
import ora from 'ora'
import { match, P } from 'ts-pattern'
import { FileWriter } from './init/file-writer'
import { ManagementApi, Region } from './management-api/api'
import { login } from './management-api/auth'
import { createAuthenticatedManagementApiClient } from './management-api/auth-client'
import { loadCredentials, saveCredentials } from './management-api/credentials'
import { printPpgInitOutput } from './platform/_'
import { successMessage } from './platform/_lib/messages'
import { determineClientOutputPath } from './utils/client-output-path'
import { printError } from './utils/prompt/utils/print'
/**
* Indicates if running in Bun runtime.
*/
export const isBun: boolean =
// @ts-ignore
!!globalThis.Bun || !!globalThis.process?.versions?.bun
export const defaultSchema = (props?: {
datasourceProvider?: ConnectorType
generatorProvider?: string
previewFeatures?: string[]
output?: string
withModel?: boolean
}) => {
const {
datasourceProvider = 'postgresql',
generatorProvider = defaultGeneratorProvider,
previewFeatures = defaultPreviewFeatures,
output = '../generated/prisma',
withModel = false,
} = props ?? {}
const aboutAccelerate = `\n// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init\n`
const isProviderCompatibleWithAccelerate = datasourceProvider !== 'sqlite'
let schema = `// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
${isProviderCompatibleWithAccelerate ? aboutAccelerate : ''}
generator client {
provider = "${generatorProvider}"
${
previewFeatures.length > 0
? ` previewFeatures = [${previewFeatures.map((feature) => `"${feature}"`).join(', ')}]\n`
: ''
} output = "${output}"
}
datasource db {
provider = "${datasourceProvider}"
}
`
// We add a model to the schema file if the user passed the --with-model flag
if (withModel) {
const defaultAttributes = `email String @unique
name String?`
switch (datasourceProvider) {
case 'mongodb':
schema += `
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
${defaultAttributes}
}
`
break
case 'cockroachdb':
schema += `
model User {
id BigInt @id @default(sequence())
${defaultAttributes}
}
`
break
default:
schema += `
model User {
id Int @id @default(autoincrement())
${defaultAttributes}
}
`
}
}
return schema
}
export const defaultEnv = async (url: string | undefined, debug = false, comments = true) => {
if (url === undefined) {
let created = false
const state =
(await ServerState.fromServerDump({ debug })) ||
((created = true), await ServerState.createExclusively({ persistenceMode: 'stateful', debug }))
if (created) {
await state.close()
}
const server = await unstable_startServer({
databasePort: state.databasePort,
dryRun: true,
port: state.port,
shadowDatabasePort: state.shadowDatabasePort,
debug,
})
url = server.ppg.url
}
let env = comments
? `# Environment variables declared in this file are NOT automatically loaded by Prisma.
# Please add \`import "dotenv/config";\` to your \`prisma.config.ts\` file, or use the Prisma CLI with Bun
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
${
url.startsWith('prisma+postgres:') && url.includes('localhost')
? `# The following \`prisma+postgres\` URL is similar to the URL produced by running a local Prisma Postgres
# server with the \`prisma dev\` CLI command, when not choosing any non-default ports or settings. The API key, unlike the
# one found in a remote Prisma Postgres URL, does not contain any sensitive information.\n\n`
: ''
}`
: ''
env += `DATABASE_URL="${url}"`
return env
}
export const defaultPort = (datasourceProvider: ConnectorType) => {
switch (datasourceProvider) {
case 'mysql':
return 3306
case 'sqlserver':
return 1433
case 'mongodb':
return 27017
case 'postgresql':
return 5432
case 'cockroachdb':
return 26257
case PRISMA_POSTGRES_PROVIDER:
return null
}
return undefined
}
export const defaultURL = (
datasourceProvider: ConnectorType,
port = defaultPort(datasourceProvider),
schema = 'public',
) => {
switch (datasourceProvider) {
case 'postgresql':
return `postgresql://johndoe:randompassword@localhost:${port}/mydb?schema=${schema}`
case 'cockroachdb':
return `postgresql://johndoe:randompassword@localhost:${port}/mydb?schema=${schema}`
case 'mysql':
return `mysql://johndoe:randompassword@localhost:${port}/mydb`
case 'sqlserver':
return `sqlserver://localhost:${port};database=mydb;user=SA;password=randompassword;`
case 'mongodb':
return `mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority`
case 'sqlite':
return 'file:./dev.db'
default:
return undefined
}
}
const defaultGitIgnore = () => {
return `node_modules
# Keep environment variables out of version control
.env
`
}
export const defaultGeneratorProvider = 'prisma-client'
export const defaultPreviewFeatures = []
function normalizePath(configPath: string) {
return JSON.stringify(configPath.replaceAll(path.sep, '/'))
}
export const defaultConfig = (props: { prismaFolder: string }) => {
const { prismaFolder } = props
const schemaPath = path.relative(process.cwd(), path.join(prismaFolder, 'schema.prisma'))
const migrationsPath = path.relative(process.cwd(), path.join(prismaFolder, 'migrations'))
return `\
// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: ${normalizePath(schemaPath)},
migrations: {
path: ${normalizePath(migrationsPath)},
},
datasource: {
url: env("DATABASE_URL"),
},
});
`
}
export class Init implements Command {
static new(): Init {
return new Init()
}
private static help = format(`
Set up a new Prisma project
${bold('Usage')}
${dim('$')} prisma init [options]
${bold('Options')}
-h, --help Display this help message
--db Provisions a fully managed Prisma Postgres database on the Prisma Data Platform.
--datasource-provider Define the datasource provider to use: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb
--generator-provider Define the generator provider to use. Default: \`prisma-client-js\`
--preview-feature Define a preview feature to use.
--output Define Prisma Client generator output path to use.
--url Define a custom datasource url
${bold('Flags')}
--with-model Add example model to created schema file
${bold('Examples')}
Set up a new \`prisma dev\`-ready (local Prisma Postgres) Prisma project
${dim('$')} prisma init
Set up a new Prisma project and specify MySQL as the datasource provider to use
${dim('$')} prisma init --datasource-provider mysql
Set up a new \`prisma dev\`-ready (local Prisma Postgres) Prisma project and specify \`prisma-client-js\` as the generator provider to use
${dim('$')} prisma init --generator-provider prisma-client-js
Set up a new \`prisma dev\`-ready (local Prisma Postgres) Prisma project and specify \`x\` and \`y\` as the preview features to use
${dim('$')} prisma init --preview-feature x --preview-feature y
Set up a new \`prisma dev\`-ready (local Prisma Postgres) Prisma project and specify \`./generated-client\` as the output path to use
${dim('$')} prisma init --output ./generated-client
Set up a new Prisma project and specify the url that will be used
${dim('$')} prisma init --url mysql://user:password@localhost:3306/mydb
Set up a new \`prisma dev\`-ready (local Prisma Postgres) Prisma project with an example model
${dim('$')} prisma init --with-model
`)
async parse(argv: string[], _config: PrismaConfigInternal): Promise<string | Error> {
const args = arg(argv, {
'--help': Boolean,
'-h': '--help',
'--url': String,
'--datasource-provider': String,
'--generator-provider': String,
'--preview-feature': [String],
'--output': String,
'--with-model': Boolean,
'--db': Boolean,
'--region': String,
'--name': String,
'--non-interactive': Boolean,
'--prompt': String,
'--vibe': String,
'--debug': Boolean,
})
if (isError(args) || args['--help']) {
return this.help()
}
const urlArg = args['--url']
if (urlArg) {
checkUnsupportedDataProxy({
cmd: 'init',
validatedConfig: { datasource: { url: urlArg } },
})
}
/**
* Validation
*/
const outputDirName = args._[0]
if (outputDirName) {
throw Error('The init command does not take any argument.')
}
const { datasourceProvider, url } = await match(args)
.with(
{
'--datasource-provider': P.when((datasourceProvider): datasourceProvider is string =>
Boolean(datasourceProvider),
),
},
(input) => {
const datasourceProvider = input['--datasource-provider'].toLowerCase()
assertDatasourceProvider(datasourceProvider)
const url = defaultURL(datasourceProvider)
return { datasourceProvider, url }
},
)
.with(
{
'--url': P.when((url): url is string => Boolean(url)),
},
async (input) => {
const url = input['--url']
const canConnect = await canConnectToDatabase(url)
if (canConnect !== true) {
const { code, message } = canConnect
// P1003 means that the db doesn't exist but we can connect
if (code !== 'P1003') {
if (code) {
throw new Error(`${code}: ${message}`)
} else {
throw new Error(message)
}
}
}
const datasourceProvider = protocolToConnectorType(`${url.split(':')[0]}:`)
return { datasourceProvider, url }
},
)
.otherwise(() => {
return {
datasourceProvider: 'postgresql' as const,
url: undefined,
}
})
const generatorProvider = args['--generator-provider']
const previewFeatures = args['--preview-feature']
const output = args['--output']
const isPpgCommand =
args['--db'] || datasourceProvider === PRISMA_POSTGRES_PROVIDER || args['--prompt'] || args['--vibe']
if (args['--debug']) {
console.log(`[isBun]`, isBun)
}
let prismaPostgresDatabaseUrl: string | undefined
let workspaceId: string | undefined
let projectId: string | undefined
let environmentId: string | undefined
const outputDir = process.cwd()
const prismaFolder = path.join(outputDir, 'prisma')
const writer = new FileWriter(outputDir)
let generatedSchema: string | undefined
let generatedName: string | undefined
if (isPpgCommand) {
const credentials = await loadCredentials()
if (!credentials) {
if (args['--non-interactive']) {
return 'Please authenticate before creating a Prisma Postgres project.'
}
console.log('This will create a project for you on console.prisma.io and requires you to be authenticated.')
const authAnswer = await confirm({
message: 'Would you like to authenticate?',
})
if (!authAnswer) {
return 'Project creation aborted. You need to authenticate to use Prisma Postgres'
}
await saveCredentials(await login())
}
if (args['--prompt'] || args['--vibe']) {
const prompt = args['--prompt'] || args['--vibe'] || ''
const spinner = ora(`Generating a Prisma Schema based on your description ${bold(prompt)} ...`).start()
try {
const serverResponseShape = Shape.Struct({
generatedSchema: Shape.String,
generatedName: Shape.String,
})
;({ generatedSchema, generatedName } = Shape.decodeUnknownSync(serverResponseShape)(
await (
await fetch(`https://prisma-generate-server.prisma.workers.dev/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
description: prompt,
}),
})
).json(),
))
} catch (e) {
spinner.fail()
throw e
}
spinner.succeed('Schema is ready')
}
console.log("Let's set up your Prisma Postgres database!")
const client = await createAuthenticatedManagementApiClient()
const api = new ManagementApi(client)
const regions = await api.getRegions()
const ppgRegionSelection =
(args['--region'] as Region) ||
(await select({
message: 'Select your region:',
default: 'us-east-1',
choices: regions.map((region) => ({
name: `${region.id} - ${region.name}`,
value: region.id as Region,
disabled: region.status !== 'available',
})),
loop: true,
}))
const projectDisplayNameAnswer =
args['--name'] ||
(await input({
message: 'Enter a project name:',
default: generatedName || 'My Prisma Project',
}))
const spinner = ora(`Creating project ${bold(projectDisplayNameAnswer)} (this may take a few seconds)...`).start()
try {
const project = await api.createProjectWithDatabase(projectDisplayNameAnswer, ppgRegionSelection)
if (!project.database) {
// This should never happen: `database` should only be `null` when
// the request body has `createDatabase: false`.
throw new Error('Missing database info in response')
}
if (!project.database.directConnection) {
// This should never happen: OpenAPI types are not entirely correct,
// `directConnection` is not independently nullable and must always
// be present if `database` is in the response body.
throw new Error('Missing connection string in response')
}
const { host, user, pass } = project.database.directConnection
prismaPostgresDatabaseUrl = `postgres://${user}:${pass}@${host}/postgres?sslmode=require`
workspaceId = project.workspace.id.replace(/^wksp_/, '')
projectId = project.id.replace(/^proj_/, '')
environmentId = project.database.id.replace(/^db_/, '')
spinner.succeed(successMessage('Your Prisma Postgres database is ready ✅'))
} catch (error) {
spinner.fail(error instanceof Error ? error.message : 'Something went wrong')
throw error
}
}
if (
fs.existsSync(path.join(outputDir, 'schema.prisma')) ||
fs.existsSync(prismaFolder) ||
fs.existsSync(path.join(prismaFolder, 'schema.prisma'))
) {
if (isPpgCommand) {
return printPpgInitOutput({
databaseUrl: prismaPostgresDatabaseUrl!,
workspaceId: workspaceId!,
projectId: projectId!,
environmentId,
isExistingPrismaProject: true,
})
}
}
if (fs.existsSync(path.join(outputDir, 'schema.prisma'))) {
console.log(
printError(`File ${bold('schema.prisma')} already exists in your project.
Please try again in a project that is not yet using Prisma.
`),
)
process.exit(1)
}
if (fs.existsSync(prismaFolder)) {
console.log(
printError(`A folder called ${bold('prisma')} already exists in your project.
Please try again in a project that is not yet using Prisma.
`),
)
process.exit(1)
}
if (fs.existsSync(path.join(prismaFolder, 'schema.prisma'))) {
console.log(
printError(`File ${bold('prisma/schema.prisma')} already exists in your project.
Please try again in a project that is not yet using Prisma.
`),
)
process.exit(1)
}
/**
* Validation successful? Let's create everything!
*/
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir)
}
if (!fs.existsSync(prismaFolder)) {
fs.mkdirSync(prismaFolder)
}
const clientOutput = output ?? determineClientOutputPath(prismaFolder)
writer.write(
path.join(prismaFolder, 'schema.prisma'),
generatedSchema ||
defaultSchema({
datasourceProvider,
generatorProvider,
previewFeatures,
output: clientOutput,
withModel: args['--with-model'],
}),
)
const databaseUrl = prismaPostgresDatabaseUrl || url
const warnings: string[] = []
writer.write(
path.join(outputDir, 'prisma.config.ts'),
defaultConfig({
prismaFolder,
}),
)
const envPath = path.join(outputDir, '.env')
if (!fs.existsSync(envPath)) {
writer.write(envPath, await defaultEnv(databaseUrl, args['--debug']))
} else {
const envFile = fs.readFileSync(envPath, { encoding: 'utf8' })
const config = dotenv.parse(envFile) // will return an object
if (Object.keys(config).includes('DATABASE_URL')) {
warnings.push(
`${yellow('warn')} Prisma would have added DATABASE_URL but it already exists in ${bold(
path.relative(outputDir, envPath),
)}.`,
)
} else {
fs.appendFileSync(
envPath,
`\n\n` + '# This was inserted by `prisma init`:\n' + (await defaultEnv(databaseUrl, args['--debug'])),
)
}
}
const gitignorePath = path.join(outputDir, '.gitignore')
try {
writer.write(gitignorePath, defaultGitIgnore(), { flag: 'wx' })
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'EEXIST') {
warnings.push(
`${yellow(
'warn',
)} You already have a ${bold('.gitignore')} file. Don't forget to add ${bold('.env')} in it to not commit any private information.`,
)
} else {
console.error('Failed to write .gitignore file, reason: ', e)
}
}
// Append the generated client to the .gitignore file regardless of whether we created it
// in the previous step.
const clientPathRelativeToOutputDir = path.relative(outputDir, path.resolve(prismaFolder, clientOutput))
try {
fs.appendFileSync(gitignorePath, `\n/${clientPathRelativeToOutputDir.replaceAll(path.sep, '/')}\n`)
} catch (e) {
console.error('Failed to append client path to .gitignore file, reason: ', e)
}
const connectExistingDatabaseSteps = `\
1. Configure your DATABASE_URL in ${green('prisma.config.ts')}
2. Run ${green(getCommandWithExecutor('prisma db pull'))} to introspect your database.`
const postgresProviders: ConnectorType[] = ['postgres', 'postgresql', 'prisma+postgres']
let setupDatabaseSection: string
if (postgresProviders.includes(datasourceProvider)) {
setupDatabaseSection = `\
Next, choose how you want to set up your database:
CONNECT EXISTING DATABASE:
${connectExistingDatabaseSteps}
CREATE NEW DATABASE:
Local: ${green('npx prisma dev')} (runs Postgres locally in your terminal)
Cloud: ${green('npx create-db')} (creates a free Prisma Postgres database)`
} else {
setupDatabaseSection = `\
Next, set up your database:
${connectExistingDatabaseSteps}`
}
const defaultOutput = `
Initialized Prisma in your project
${writer.format({
level: 0,
printHeadersFromLevel: 1,
indentSize: 2,
})}
${warnings.length > 0 && logger.should.warn() ? `\n${warnings.join('\n')}\n` : ''}
${setupDatabaseSection}
Then, define your models in ${green('prisma/schema.prisma')} and run ${green(getCommandWithExecutor('prisma migrate dev'))} to apply your schema.
Learn more: ${link('https://pris.ly/getting-started')}
`
return isPpgCommand
? printPpgInitOutput({
databaseUrl: prismaPostgresDatabaseUrl!,
workspaceId: workspaceId!,
projectId: projectId!,
environmentId,
})
: defaultOutput
}
// help message
public help(error?: string): string | HelpError {
if (error) {
return new HelpError(`\n${bold(red(`!`))} ${error}\n${Init.help}`)
}
return Init.help
}
}
// order matters for the error message.
const DATASOURCE_PROVIDERS = [
'postgresql',
'mysql',
'sqlite',
'sqlserver',
'mongodb',
'cockroachdb',
'prismapostgres',
'prisma+postgres',
] as const
function assertDatasourceProvider(thing: unknown): asserts thing is ConnectorType {
if (typeof thing !== 'string' || !DATASOURCE_PROVIDERS.includes(thing as never)) {
throw new Error(
`Provider "${thing}" is invalid or not supported. Try again with ${DATASOURCE_PROVIDERS.slice(0, -1)
.map((p) => `"${p}"`)
.join(', ')} or "${DATASOURCE_PROVIDERS.at(-1)}".`,
)
}
}