Skip to content
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

feat: Neon as database Provider #1976

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions .changeset/purple-deers-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-t3-app": minor
---

Add support for Neon as Postgresql Database Provider
18 changes: 17 additions & 1 deletion cli/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface CliResults {
packages: AvailablePackages[];
flags: CliFlags;
databaseProvider: DatabaseProvider;
drizzleDatabaseProvider: DatabaseProvider;
}

const defaultOptions: CliResults = {
Expand All @@ -64,6 +65,7 @@ const defaultOptions: CliResults = {
dbProvider: "sqlite",
},
databaseProvider: "sqlite",
drizzleDatabaseProvider: "sqlite",
};

export const runCli = async (): Promise<CliResults> => {
Expand Down Expand Up @@ -296,6 +298,7 @@ export const runCli = async (): Promise<CliResults> => {
{ value: "mysql", label: "MySQL" },
{ value: "postgres", label: "PostgreSQL" },
{ value: "planetscale", label: "PlanetScale" },
{ value: "neon", label: "Neon" },
],
initialValue: "sqlite",
});
Expand Down Expand Up @@ -342,18 +345,31 @@ export const runCli = async (): Promise<CliResults> => {
if (project.database === "prisma") packages.push("prisma");
if (project.database === "drizzle") packages.push("drizzle");

// Preserve the original databaseProvider for Prisma and other logic
const originalDatabaseProvider = project.databaseProvider;

// Map Neon to postgres and Planetscale to mysql when Drizzle is selected
const drizzleDatabaseProvider = (
originalDatabaseProvider === "neon"
? "postgres"
: originalDatabaseProvider === "planetscale"
? "mysql"
: originalDatabaseProvider
) as DatabaseProvider;

return {
appName: project.name ?? cliResults.appName,
packages,
databaseProvider:
(project.databaseProvider as DatabaseProvider) || "sqlite",
(originalDatabaseProvider as DatabaseProvider) || "sqlite",
flags: {
...cliResults.flags,
appRouter: project.appRouter ?? cliResults.flags.appRouter,
noGit: !project.git || cliResults.flags.noGit,
noInstall: !project.install || cliResults.flags.noInstall,
importAlias: project.importAlias ?? cliResults.flags.importAlias,
},
drizzleDatabaseProvider, // Pass this separately to the Drizzle installer
};
} catch (err) {
// If the user is not calling create-t3-app from an interactive terminal, inquirer will throw an IsTTYError
Expand Down
4 changes: 4 additions & 0 deletions cli/src/helpers/createProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface CreateProjectOptions {
importAlias: string;
appRouter: boolean;
databaseProvider: DatabaseProvider;
drizzleDatabaseProvider: DatabaseProvider;
}

export const createProject = async ({
Expand All @@ -33,6 +34,7 @@ export const createProject = async ({
noInstall,
appRouter,
databaseProvider,
drizzleDatabaseProvider,
}: CreateProjectOptions) => {
const pkgManager = getUserPkgManager();
const projectDir = path.resolve(process.cwd(), projectName);
Expand All @@ -46,6 +48,7 @@ export const createProject = async ({
noInstall,
appRouter,
databaseProvider,
drizzleDatabaseProvider,
});

// Install the selected packages
Expand All @@ -58,6 +61,7 @@ export const createProject = async ({
noInstall,
appRouter,
databaseProvider,
drizzleDatabaseProvider,
});

// Select necessary _app,index / layout,page files
Expand Down
2 changes: 2 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const main = async () => {
packages,
flags: { noGit, noInstall, importAlias, appRouter },
databaseProvider,
drizzleDatabaseProvider,
} = await runCli();

const usePackages = buildPkgInstallerMap(packages, databaseProvider);
Expand All @@ -52,6 +53,7 @@ const main = async () => {
scopedAppName,
packages: usePackages,
databaseProvider,
drizzleDatabaseProvider,
importAlias,
noInstall,
appRouter,
Expand Down
1 change: 1 addition & 0 deletions cli/src/installers/dependencyVersionMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const dependencyVersionMap = {
"@planetscale/database": "^1.19.0",
postgres: "^3.4.4",
"@libsql/client": "^0.9.0",
"@neondatabase/serverless": "^0.9.4",

// TailwindCSS
tailwindcss: "^3.4.3",
Expand Down
15 changes: 7 additions & 8 deletions cli/src/installers/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const drizzleInstaller: Installer = ({
projectDir,
packages,
scopedAppName,
databaseProvider,
drizzleDatabaseProvider,
}) => {
const devPackages: AvailableDependencies[] = [
"drizzle-kit",
Expand All @@ -31,10 +31,11 @@ export const drizzleInstaller: Installer = ({
{
planetscale: "@planetscale/database",
mysql: "mysql2",
neon: "postgres",
postgres: "postgres",
sqlite: "@libsql/client",
} as const
)[databaseProvider],
)[drizzleDatabaseProvider],
],
devMode: false,
});
Expand All @@ -43,18 +44,16 @@ export const drizzleInstaller: Installer = ({

const configFile = path.join(
extrasDir,
`config/drizzle-config-${
databaseProvider === "planetscale" ? "mysql" : databaseProvider
}.ts`
`config/drizzle-config-${drizzleDatabaseProvider}.ts`
);
const configDest = path.join(projectDir, "drizzle.config.ts");

const schemaSrc = path.join(
extrasDir,
"src/server/db/schema-drizzle",
packages?.nextAuth.inUse
? `with-auth-${databaseProvider}.ts`
: `base-${databaseProvider}.ts`
? `with-auth-${drizzleDatabaseProvider}.ts`
: `base-${drizzleDatabaseProvider}.ts`
);
const schemaDest = path.join(projectDir, "src/server/db/schema.ts");

Expand All @@ -71,7 +70,7 @@ export const drizzleInstaller: Installer = ({

const clientSrc = path.join(
extrasDir,
`src/server/db/index-drizzle/with-${databaseProvider}.ts`
`src/server/db/index-drizzle/with-${drizzleDatabaseProvider}.ts`
);
const clientDest = path.join(projectDir, "src/server/db/index.ts");

Expand Down
6 changes: 5 additions & 1 deletion cli/src/installers/envVars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const envVariablesInstaller: Installer = ({
} else {
if (usingAuth) envFile = "with-auth.js";
}

console.log("using env file:", envFile);
if (envFile !== "") {
const envSchemaSrc = path.join(
PKG_ROOT,
Expand Down Expand Up @@ -87,6 +87,10 @@ DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}'`;
content = `# Get the Database URL from the "prisma" dropdown selector in PlanetScale.
DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?sslaccept=strict'`;
}
} else if (databaseProvider === "neon") {
content += `# Get the database connection details from the Connection Details widget on the Neon Dashboard.
# Select a branch, a compute, a database, and a role. A connection string is constructed for you
DATABASE_URL="postgresql://YOUR_POSTGRES_CONNECTION_STRING_HERE?sslmode=require"`;
} else if (databaseProvider === "mysql") {
content += `DATABASE_URL="mysql://root:password@localhost:3306/${scopedAppName}"`;
} else if (databaseProvider === "postgres") {
Expand Down
2 changes: 2 additions & 0 deletions cli/src/installers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const databaseProviders = [
"postgres",
"sqlite",
"planetscale",
"neon",
] as const;
export type DatabaseProvider = (typeof databaseProviders)[number];

Expand All @@ -39,6 +40,7 @@ export interface InstallerOptions {
projectName: string;
scopedAppName: string;
databaseProvider: DatabaseProvider;
drizzleDatabaseProvider: DatabaseProvider;
}

export type Installer = (opts: InstallerOptions) => void;
Expand Down
14 changes: 13 additions & 1 deletion cli/src/installers/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,24 @@ export const prismaInstaller: Installer = ({
devMode: false,
});

if (databaseProvider === "neon")
addPackageDependency({
projectDir,
dependencies: ["@neondatabase/serverless"],
devMode: false,
});

const extrasDir = path.join(PKG_ROOT, "template/extras");

const schemaSrc = path.join(
extrasDir,
"prisma/schema",
`${packages?.nextAuth.inUse ? "with-auth" : "base"}${
databaseProvider === "planetscale" ? "-planetscale" : ""
databaseProvider === "planetscale"
? "-planetscale"
: databaseProvider === "neon"
? "-neon"
: ""
}.prisma`
);
let schemaText = fs.readFileSync(schemaSrc, "utf-8");
Expand All @@ -46,6 +57,7 @@ export const prismaInstaller: Installer = ({
mysql: "mysql",
postgres: "postgresql",
planetscale: "mysql",
neon: "postgresql",
}[databaseProvider]
}"`
);
Expand Down
21 changes: 21 additions & 0 deletions cli/template/extras/prisma/schema/base-neon.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Post {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([name])
}
74 changes: 74 additions & 0 deletions cli/template/extras/prisma/schema/with-auth-neon.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}

model Post {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

createdBy User @relation(fields: [createdById], references: [id])
createdById String

@@index([name])
@@index([createdById])
}

// Necessary for NextAuth.js
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([provider, providerAccountId])
@@index([userId])
}

model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
}

model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
posts Post[]
}

model VerificationToken {
identifier String
token String @unique
expires DateTime

@@unique([identifier, token])
}
29 changes: 29 additions & 0 deletions cli/template/extras/src/server/db/db-prisma-neon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { neon } from "@neondatabase/serverless";
import { PrismaNeonHTTP } from "@prisma/adapter-neon";
import { PrismaClient } from "@prisma/client";

import { env } from "~/env";

// Initialize Neon client using the DATABASE_URL from the environment variables
const sql = neon(env.DATABASE_URL);

// Set up the Prisma adapter for Neon
const adapter = new PrismaNeonHTTP(sql);

// Create a new Prisma client instance with the Neon adapter
const createPrismaClient = () =>
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
adapter,
});

// Global variable to store the Prisma client instance across module reloads
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | undefined;
};

// Export the Prisma client instance, reusing it if it already exists
export const db = globalForPrisma.prisma ?? createPrismaClient();

if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
Loading