diff --git a/.changeset/lemon-deers-grin.md b/.changeset/lemon-deers-grin.md new file mode 100644 index 0000000000..7712ca969c --- /dev/null +++ b/.changeset/lemon-deers-grin.md @@ -0,0 +1,5 @@ +--- +"create-t3-app": minor +--- + +add more db options diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index cb12c5c5e3..86eca04c05 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,8 +29,9 @@ jobs: prisma: ["true", "false"] appRouter: ["true", "false"] drizzle: ["true", "false"] + dbType: ["planetscale", "sqlite", "mysql", "postgres"] - name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}" + name: "Build and Start T3 App ${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }}" steps: - uses: actions/checkout@v3 with: @@ -39,7 +40,7 @@ jobs: - name: Check valid matrix id: matrix-valid run: | - echo "continue=${{ matrix.prisma == 'false' || matrix.drizzle == 'false' }}" >> $GITHUB_OUTPUT + echo "continue=${{ (matrix.prisma == 'false' || matrix.drizzle == 'false') && (matrix.drizzle == 'true' || matrix.prisma == 'true' || matrix.dbType == 'sqlite') }}" >> $GITHUB_OUTPUT - name: Install Node.js if: ${{ steps.matrix-valid.outputs.continue == 'true' }} @@ -79,9 +80,9 @@ jobs: # has to be scaffolded outside the CLI project so that no lint/tsconfig are leaking # through. this way it ensures that it is the app's configs that are being used # FIXME: this is a bit hacky, would rather have --packages=trpc,tailwind,... but not sure how to setup the matrix for that - - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} + - run: cd cli && pnpm start ../../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} --noGit --CI --trpc=${{ matrix.trpc }} --tailwind=${{ matrix.tailwind }} --nextAuth=${{ matrix.nextAuth }} --prisma=${{ matrix.prisma }} --drizzle=${{ matrix.drizzle }} --appRouter=${{ matrix.appRouter }} --dbProvider=${{ matrix.dbType }} if: ${{ steps.matrix-valid.outputs.continue == 'true' }} - - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }} && pnpm build + - run: cd ../ci-${{ matrix.trpc }}-${{ matrix.tailwind }}-${{ matrix.nextAuth }}-${{ matrix.prisma }}-${{ matrix.drizzle}}-${{ matrix.appRouter }}-${{ matrix.dbType }} && pnpm build if: ${{ steps.matrix-valid.outputs.continue == 'true' }} env: NEXTAUTH_SECRET: foo diff --git a/cli/package.json b/cli/package.json index bf0c7365bc..a990435c13 100644 --- a/cli/package.json +++ b/cli/package.json @@ -65,6 +65,7 @@ "@auth/drizzle-adapter": "^0.3.6", "@next-auth/prisma-adapter": "^1.0.7", "@planetscale/database": "^1.11.0", + "@prisma/adapter-planetscale": "^5.6.0", "@prisma/client": "^5.6.0", "@t3-oss/env-nextjs": "^0.7.1", "@tanstack/react-query": "^4.36.1", diff --git a/cli/src/cli/index.ts b/cli/src/cli/index.ts index fe8c383f66..1cbba39824 100644 --- a/cli/src/cli/index.ts +++ b/cli/src/cli/index.ts @@ -3,7 +3,11 @@ import chalk from "chalk"; import { Command } from "commander"; import { CREATE_T3_APP, DEFAULT_APP_NAME } from "~/consts.js"; -import { type AvailablePackages } from "~/installers/index.js"; +import { + databaseProviders, + type AvailablePackages, + type DatabaseProvider, +} from "~/installers/index.js"; import { getVersion } from "~/utils/getT3Version.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; import { IsTTYError } from "~/utils/isTTYError.js"; @@ -37,6 +41,7 @@ interface CliResults { appName: string; packages: AvailablePackages[]; flags: CliFlags; + databaseProvider: DatabaseProvider; } const defaultOptions: CliResults = { @@ -55,6 +60,7 @@ const defaultOptions: CliResults = { importAlias: "~/", appRouter: false, }, + databaseProvider: "sqlite", }; export const runCli = async (): Promise => { @@ -124,6 +130,13 @@ export const runCli = async (): Promise => { "Explicitly tell the CLI to use a custom import alias", defaultOptions.flags.importAlias ) + .option( + "--dbProvider [provider]", + `Choose a database provider to use. Possible values: ${databaseProviders.join( + ", " + )}`, + defaultOptions.flags.importAlias + ) .option( "--appRouter [boolean]", "Explicitly tell the CLI to use the new Next.js app router", @@ -176,6 +189,10 @@ export const runCli = async (): Promise => { process.exit(0); } + cliResults.databaseProvider = cliResults.packages.includes("drizzle") + ? "planetscale" + : "sqlite"; + return cliResults; } @@ -262,6 +279,19 @@ export const runCli = async (): Promise => { initialValue: false, }); }, + databaseProvider: ({ results }) => { + if (results.database === "none") return; + return p.select({ + message: "What database provider would you like to use?", + options: [ + { value: "sqlite", label: "SQLite" }, + { value: "mysql", label: "MySQL" }, + { value: "postgres", label: "PostgreSQL" }, + { value: "planetscale", label: "Planetscale" }, + ], + initialValue: "sqlite", + }); + }, ...(!cliResults.flags.noGit && { git: () => { return p.confirm({ @@ -307,6 +337,8 @@ export const runCli = async (): Promise => { return { appName: project.name ?? cliResults.appName, packages, + databaseProvider: + (project.databaseProvider as DatabaseProvider) || "sqlite", flags: { ...cliResults.flags, appRouter: project.appRouter ?? cliResults.flags.appRouter, diff --git a/cli/src/helpers/createProject.ts b/cli/src/helpers/createProject.ts index d8f20a88f7..81b385485f 100644 --- a/cli/src/helpers/createProject.ts +++ b/cli/src/helpers/createProject.ts @@ -10,7 +10,10 @@ import { selectLayoutFile, selectPageFile, } from "~/helpers/selectBoilerplate.js"; -import { type PkgInstallerMap } from "~/installers/index.js"; +import { + type DatabaseProvider, + type PkgInstallerMap, +} from "~/installers/index.js"; import { getUserPkgManager } from "~/utils/getUserPkgManager.js"; interface CreateProjectOptions { @@ -20,6 +23,7 @@ interface CreateProjectOptions { noInstall: boolean; importAlias: string; appRouter: boolean; + databaseProvider: DatabaseProvider; } export const createProject = async ({ @@ -28,6 +32,7 @@ export const createProject = async ({ packages, noInstall, appRouter, + databaseProvider, }: CreateProjectOptions) => { const pkgManager = getUserPkgManager(); const projectDir = path.resolve(process.cwd(), projectName); @@ -40,6 +45,7 @@ export const createProject = async ({ scopedAppName, noInstall, appRouter, + databaseProvider, }); // Install the selected packages @@ -51,6 +57,7 @@ export const createProject = async ({ packages, noInstall, appRouter, + databaseProvider, }); // Select necessary _app,index / layout,page files diff --git a/cli/src/helpers/logNextSteps.ts b/cli/src/helpers/logNextSteps.ts index a51d31ffc0..c67a4c5216 100644 --- a/cli/src/helpers/logNextSteps.ts +++ b/cli/src/helpers/logNextSteps.ts @@ -11,9 +11,15 @@ export const logNextSteps = async ({ appRouter, noInstall, projectDir, + databaseProvider, }: Pick< InstallerOptions, - "projectName" | "packages" | "noInstall" | "projectDir" | "appRouter" + | "projectName" + | "packages" + | "noInstall" + | "projectDir" + | "appRouter" + | "databaseProvider" >) => { const pkgManager = getUserPkgManager(); @@ -28,6 +34,10 @@ export const logNextSteps = async ({ } } + if (["postgres", "mysql"].includes(databaseProvider)) { + logger.info(" ./start-database.sh"); + } + if (packages?.prisma.inUse || packages?.drizzle.inUse) { if (["npm", "bun"].includes(pkgManager)) { logger.info(` ${pkgManager} run db:push`); diff --git a/cli/src/index.ts b/cli/src/index.ts index 5657401ea2..c4774954cc 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -37,9 +37,10 @@ const main = async () => { appName, packages, flags: { noGit, noInstall, importAlias, appRouter }, + databaseProvider, } = await runCli(); - const usePackages = buildPkgInstallerMap(packages); + const usePackages = buildPkgInstallerMap(packages, databaseProvider); // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] const [scopedAppName, appDir] = parseNameAndPath(appName); @@ -48,6 +49,7 @@ const main = async () => { projectName: appDir, scopedAppName, packages: usePackages, + databaseProvider, importAlias, noInstall, appRouter, @@ -97,6 +99,7 @@ const main = async () => { appRouter, noInstall, projectDir, + databaseProvider, }); process.exit(0); diff --git a/cli/src/installers/dbContainer.ts b/cli/src/installers/dbContainer.ts new file mode 100644 index 0000000000..8a72ea0a40 --- /dev/null +++ b/cli/src/installers/dbContainer.ts @@ -0,0 +1,20 @@ +import fs from "fs"; +import path from "path"; + +import { PKG_ROOT } from "~/consts.js"; +import { type Installer } from "~/installers/index.js"; + +export const dbContainerInstaller: Installer = ({ + projectDir, + databaseProvider, + projectName, +}) => { + const scriptSrc = path.join( + PKG_ROOT, + `template/extras/start-database/${databaseProvider}.sh` + ); + const scriptText = fs.readFileSync(scriptSrc, "utf-8"); + const scriptDest = path.join(projectDir, "start-database.sh"); + fs.writeFileSync(scriptDest, scriptText.replaceAll("project1", projectName)); + fs.chmodSync(scriptDest, "755"); +}; diff --git a/cli/src/installers/dependencyVersionMap.ts b/cli/src/installers/dependencyVersionMap.ts index 03a592643c..6cd177d4a0 100644 --- a/cli/src/installers/dependencyVersionMap.ts +++ b/cli/src/installers/dependencyVersionMap.ts @@ -11,12 +11,16 @@ export const dependencyVersionMap = { // Prisma prisma: "^5.6.0", "@prisma/client": "^5.6.0", + "@prisma/adapter-planetscale": "^5.6.0", // Drizzle "drizzle-orm": "^0.29.3", "drizzle-kit": "^0.20.9", mysql2: "^3.6.1", "@planetscale/database": "^1.11.0", + postgres: "^3.4.3", + "@types/better-sqlite3": "^7.6.6", + "better-sqlite3": "^9.0.0", // TailwindCSS tailwindcss: "^3.3.5", diff --git a/cli/src/installers/drizzle.ts b/cli/src/installers/drizzle.ts index 411c94458f..65f59c23f5 100644 --- a/cli/src/installers/drizzle.ts +++ b/cli/src/installers/drizzle.ts @@ -5,34 +5,55 @@ import { type PackageJson } from "type-fest"; import { PKG_ROOT } from "~/consts.js"; import { type Installer } from "~/installers/index.js"; import { addPackageDependency } from "~/utils/addPackageDependency.js"; +import { type AvailableDependencies } from "./dependencyVersionMap.js"; export const drizzleInstaller: Installer = ({ projectDir, packages, scopedAppName, + databaseProvider, }) => { + const devPackages: AvailableDependencies[] = ["drizzle-kit"]; + if (databaseProvider === "planetscale") devPackages.push("mysql2"); + if (databaseProvider === "sqlite") devPackages.push("@types/better-sqlite3"); + addPackageDependency({ projectDir, - dependencies: ["drizzle-kit", "mysql2"], + dependencies: devPackages, devMode: true, }); addPackageDependency({ projectDir, - dependencies: ["drizzle-orm", "@planetscale/database"], + dependencies: [ + "drizzle-orm", + ( + { + planetscale: "@planetscale/database", + mysql: "mysql2", + postgres: "postgres", + sqlite: "better-sqlite3", + } as const + )[databaseProvider], + ], devMode: false, }); const extrasDir = path.join(PKG_ROOT, "template/extras"); - const configFile = path.join(extrasDir, "config/drizzle.config.ts"); + const configFile = path.join( + extrasDir, + `config/drizzle-config-${ + databaseProvider === "planetscale" ? "mysql" : databaseProvider + }.ts` + ); const configDest = path.join(projectDir, "drizzle.config.ts"); const schemaSrc = path.join( extrasDir, - "src/server/db", + "src/server/db/schema-drizzle", packages?.nextAuth.inUse - ? "drizzle-schema-auth.ts" - : "drizzle-schema-base.ts" + ? `with-auth-${databaseProvider}.ts` + : `base-${databaseProvider}.ts` ); const schemaDest = path.join(projectDir, "src/server/db/schema.ts"); @@ -42,10 +63,15 @@ export const drizzleInstaller: Installer = ({ "project1_${name}", `${scopedAppName}_\${name}` ); + let configContent = fs.readFileSync(configFile, "utf-8"); + configContent = configContent.replace("project1_*", `${scopedAppName}_*`); - const clientSrc = path.join(extrasDir, "src/server/db/index-drizzle.ts"); + const clientSrc = path.join( + extrasDir, + `src/server/db/index-drizzle/with-${databaseProvider}.ts` + ); const clientDest = path.join(projectDir, "src/server/db/index.ts"); // add db:push script to package.json @@ -54,7 +80,14 @@ export const drizzleInstaller: Installer = ({ const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson; packageJsonContent.scripts = { ...packageJsonContent.scripts, - "db:push": "drizzle-kit push:mysql", + "db:push": `drizzle-kit push:${ + { + postgres: "pg", + sqlite: "sqlite", + mysql: "mysql", + planetscale: "mysql", + }[databaseProvider] + }`, "db:studio": "drizzle-kit studio", }; diff --git a/cli/src/installers/envVars.ts b/cli/src/installers/envVars.ts index 23a9e40fad..38198d9ee4 100644 --- a/cli/src/installers/envVars.ts +++ b/cli/src/installers/envVars.ts @@ -2,25 +2,32 @@ import path from "path"; import fs from "fs-extra"; import { PKG_ROOT } from "~/consts.js"; -import { type Installer } from "~/installers/index.js"; - -export const envVariablesInstaller: Installer = ({ projectDir, packages }) => { +import { type DatabaseProvider, type Installer } from "~/installers/index.js"; + +export const envVariablesInstaller: Installer = ({ + projectDir, + packages, + databaseProvider, + projectName, +}) => { const usingAuth = packages?.nextAuth.inUse; const usingPrisma = packages?.prisma.inUse; const usingDrizzle = packages?.drizzle.inUse; const usingDb = usingPrisma || usingDrizzle; - const envContent = getEnvContent(!!usingAuth, !!usingPrisma, !!usingDrizzle); + const envContent = getEnvContent( + !!usingAuth, + !!usingPrisma, + !!usingDrizzle, + databaseProvider, + projectName + ); - const envFile = - usingAuth && usingDb - ? "with-auth-db.js" - : usingAuth - ? "with-auth.js" - : usingDb - ? "with-db.js" - : ""; + let envFile = ""; + if (usingAuth && usingDb) envFile = "with-auth-db.js"; + else if (usingAuth) envFile = "with-auth.js"; + else if (usingDb) envFile = "with-db.js"; if (envFile !== "") { const envSchemaSrc = path.join( @@ -28,8 +35,15 @@ export const envVariablesInstaller: Installer = ({ projectDir, packages }) => { "template/extras/src/env", envFile ); + const envFileText = fs.readFileSync(envSchemaSrc, "utf-8"); const envSchemaDest = path.join(projectDir, "src/env.js"); - fs.copySync(envSchemaSrc, envSchemaDest); + fs.writeFileSync( + envSchemaDest, + databaseProvider === "sqlite" + ? envFileText.replace("\n .url()", "") + : envFileText, + "utf-8" + ); } const envDest = path.join(projectDir, ".env"); @@ -42,7 +56,9 @@ export const envVariablesInstaller: Installer = ({ projectDir, packages }) => { const getEnvContent = ( usingAuth: boolean, usingPrisma: boolean, - usingDrizzle: boolean + usingDrizzle: boolean, + databaseProvider: DatabaseProvider, + projectName: string ) => { let content = ` # When adding additional environment variables, the schema in "/src/env.js" @@ -55,16 +71,30 @@ const getEnvContent = ( content += ` # Prisma # https://www.prisma.io/docs/reference/database-reference/connection-urls#env -DATABASE_URL="file:./db.sqlite" `; - if (usingDrizzle) { - content += ` -# Drizzle -# Get the Database URL from the "prisma" dropdown selector in PlanetScale. + if (usingDrizzle) content += "\n# Drizzle\n"; + + if (usingPrisma || usingDrizzle) { + if (databaseProvider === "planetscale") { + if (usingDrizzle) { + content += `Get the Database URL from the "prisma" dropdown selector in PlanetScale. # Change the query params at the end of the URL to "?ssl={"rejectUnauthorized":true}" -DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}' -`; +DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?ssl={"rejectUnauthorized":true}'`; + } else { + content = `Get the Database URL from the "prisma" dropdown selector in PlanetScale. +DATABASE_URL='mysql://YOUR_MYSQL_URL_HERE?sslaccept=strict'`; + } + } else if (databaseProvider === "mysql") { + content += `DATABASE_URL="mysql://root:password@localhost:3306/${projectName}"`; + } else if (databaseProvider === "postgres") { + content += `DATABASE_URL="postgresql://postgres:password@localhost:5432/${projectName}"`; + } else if (databaseProvider === "sqlite") { + content += usingPrisma + ? 'DATABASE_URL="file:./db.sqlite"' + : 'DATABASE_URL="db.sqlite"'; + } + content += "\n"; } if (usingAuth) diff --git a/cli/src/installers/index.ts b/cli/src/installers/index.ts index 8ab83c2c1b..83a4f0d880 100644 --- a/cli/src/installers/index.ts +++ b/cli/src/installers/index.ts @@ -4,6 +4,7 @@ import { prismaInstaller } from "~/installers/prisma.js"; import { tailwindInstaller } from "~/installers/tailwind.js"; import { trpcInstaller } from "~/installers/trpc.js"; import { type PackageManager } from "~/utils/getUserPkgManager.js"; +import { dbContainerInstaller } from "./dbContainer.js"; import { drizzleInstaller } from "./drizzle.js"; // Turning this into a const allows the list to be iterated over for programatically creating prompt options @@ -15,9 +16,18 @@ export const availablePackages = [ "tailwind", "trpc", "envVariables", + "dbContainer", ] as const; export type AvailablePackages = (typeof availablePackages)[number]; +export const databaseProviders = [ + "mysql", + "postgres", + "sqlite", + "planetscale", +] as const; +export type DatabaseProvider = (typeof databaseProviders)[number]; + export interface InstallerOptions { projectDir: string; pkgManager: PackageManager; @@ -26,6 +36,7 @@ export interface InstallerOptions { appRouter?: boolean; projectName: string; scopedAppName: string; + databaseProvider: DatabaseProvider; } export type Installer = (opts: InstallerOptions) => void; @@ -38,7 +49,8 @@ export type PkgInstallerMap = { }; export const buildPkgInstallerMap = ( - packages: AvailablePackages[] + packages: AvailablePackages[], + databaseProvider: DatabaseProvider ): PkgInstallerMap => ({ nextAuth: { inUse: packages.includes("nextAuth"), @@ -60,6 +72,10 @@ export const buildPkgInstallerMap = ( inUse: packages.includes("trpc"), installer: trpcInstaller, }, + dbContainer: { + inUse: ["mysql", "postgres"].includes(databaseProvider), + installer: dbContainerInstaller, + }, envVariables: { inUse: true, installer: envVariablesInstaller, diff --git a/cli/src/installers/prisma.ts b/cli/src/installers/prisma.ts index 6001b3067d..db2245e857 100644 --- a/cli/src/installers/prisma.ts +++ b/cli/src/installers/prisma.ts @@ -6,7 +6,11 @@ import { PKG_ROOT } from "~/consts.js"; import { type Installer } from "~/installers/index.js"; import { addPackageDependency } from "~/utils/addPackageDependency.js"; -export const prismaInstaller: Installer = ({ projectDir, packages }) => { +export const prismaInstaller: Installer = ({ + projectDir, + packages, + databaseProvider, +}) => { addPackageDependency({ projectDir, dependencies: ["prisma"], @@ -17,17 +21,48 @@ export const prismaInstaller: Installer = ({ projectDir, packages }) => { dependencies: ["@prisma/client"], devMode: false, }); + if (databaseProvider === "planetscale") + addPackageDependency({ + projectDir, + dependencies: ["@prisma/adapter-planetscale", "@planetscale/database"], + devMode: false, + }); const extrasDir = path.join(PKG_ROOT, "template/extras"); const schemaSrc = path.join( extrasDir, "prisma/schema", - packages?.nextAuth.inUse ? "with-auth.prisma" : "base.prisma" + `${packages?.nextAuth.inUse ? "with-auth" : "base"}${ + databaseProvider === "planetscale" ? "-planetscale" : "" + }.prisma` ); + let schemaText = fs.readFileSync(schemaSrc, "utf-8"); + if (databaseProvider !== "sqlite") { + schemaText = schemaText.replace( + 'provider = "sqlite"', + `provider = "${ + { + mysql: "mysql", + postgres: "postgresql", + planetscale: "mysql", + }[databaseProvider] + }"` + ); + if (["mysql", "planetscale"].includes(databaseProvider)) { + schemaText = schemaText.replace("// @db.Text", "@db.Text"); + } + } const schemaDest = path.join(projectDir, "prisma/schema.prisma"); + fs.mkdirSync(path.dirname(schemaDest), { recursive: true }); + fs.writeFileSync(schemaDest, schemaText); - const clientSrc = path.join(extrasDir, "src/server/db/db-prisma.ts"); + const clientSrc = path.join( + extrasDir, + databaseProvider === "planetscale" + ? "src/server/db/db-prisma-planetscale.ts" + : "src/server/db/db-prisma.ts" + ); const clientDest = path.join(projectDir, "src/server/db.ts"); // add postinstall and push script to package.json @@ -41,7 +76,6 @@ export const prismaInstaller: Installer = ({ projectDir, packages }) => { "db:studio": "prisma studio", }; - fs.copySync(schemaSrc, schemaDest); fs.copySync(clientSrc, clientDest); fs.writeJSONSync(packageJsonPath, packageJsonContent, { spaces: 2, diff --git a/cli/template/extras/config/drizzle.config.ts b/cli/template/extras/config/drizzle-config-mysql.ts similarity index 100% rename from cli/template/extras/config/drizzle.config.ts rename to cli/template/extras/config/drizzle-config-mysql.ts diff --git a/cli/template/extras/config/drizzle-config-postgres.ts b/cli/template/extras/config/drizzle-config-postgres.ts new file mode 100644 index 0000000000..c12ea0f284 --- /dev/null +++ b/cli/template/extras/config/drizzle-config-postgres.ts @@ -0,0 +1,12 @@ +import { type Config } from "drizzle-kit"; + +import { env } from "~/env.js"; + +export default { + schema: "./src/server/db/schema.ts", + driver: "pg", + dbCredentials: { + connectionString: env.DATABASE_URL, + }, + tablesFilter: ["project1_*"], +} satisfies Config; diff --git a/cli/template/extras/config/drizzle-config-sqlite.ts b/cli/template/extras/config/drizzle-config-sqlite.ts new file mode 100644 index 0000000000..5296a0cd14 --- /dev/null +++ b/cli/template/extras/config/drizzle-config-sqlite.ts @@ -0,0 +1,12 @@ +import { type Config } from "drizzle-kit"; + +import { env } from "~/env.js"; + +export default { + schema: "./src/server/db/schema.ts", + driver: "better-sqlite", + dbCredentials: { + url: env.DATABASE_URL, + }, + tablesFilter: ["project1_*"], +} satisfies Config; diff --git a/cli/template/extras/prisma/schema/base-planetscale.prisma b/cli/template/extras/prisma/schema/base-planetscale.prisma new file mode 100644 index 0000000000..0e57946b97 --- /dev/null +++ b/cli/template/extras/prisma/schema/base-planetscale.prisma @@ -0,0 +1,24 @@ +// 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 = "mysql" + url = env("DATABASE_URL") + + // Do not use foreign keys (PlanetScale does not support them) + relationMode = "prisma" +} + +model Post { + id Int @id @default(autoincrement()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([name]) +} diff --git a/cli/template/extras/prisma/schema/with-auth-planetscale.prisma b/cli/template/extras/prisma/schema/with-auth-planetscale.prisma new file mode 100644 index 0000000000..00a24dd180 --- /dev/null +++ b/cli/template/extras/prisma/schema/with-auth-planetscale.prisma @@ -0,0 +1,77 @@ +// 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 = "mysql" + url = env("DATABASE_URL") + + // Do not use foreign keys (PlanetScale does not support them) + relationMode = "prisma" +} + +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 Next auth +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]) +} diff --git a/cli/template/extras/src/server/auth-app/with-drizzle.ts b/cli/template/extras/src/server/auth-app/with-drizzle.ts index 995acdc9fb..6c2dc4f826 100644 --- a/cli/template/extras/src/server/auth-app/with-drizzle.ts +++ b/cli/template/extras/src/server/auth-app/with-drizzle.ts @@ -9,7 +9,7 @@ import DiscordProvider from "next-auth/providers/discord"; import { env } from "~/env"; import { db } from "~/server/db"; -import { mysqlTable } from "~/server/db/schema"; +import { createTable } from "~/server/db/schema"; /** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` @@ -47,7 +47,7 @@ export const authOptions: NextAuthOptions = { }, }), }, - adapter: DrizzleAdapter(db, mysqlTable) as Adapter, + adapter: DrizzleAdapter(db, createTable) as Adapter, providers: [ DiscordProvider({ clientId: env.DISCORD_CLIENT_ID, diff --git a/cli/template/extras/src/server/auth-pages/with-drizzle.ts b/cli/template/extras/src/server/auth-pages/with-drizzle.ts index 36214a09b2..6e444ee76b 100644 --- a/cli/template/extras/src/server/auth-pages/with-drizzle.ts +++ b/cli/template/extras/src/server/auth-pages/with-drizzle.ts @@ -10,7 +10,7 @@ import DiscordProvider from "next-auth/providers/discord"; import { env } from "~/env"; import { db } from "~/server/db"; -import { mysqlTable } from "~/server/db/schema"; +import { createTable } from "~/server/db/schema"; /** * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session` @@ -48,7 +48,7 @@ export const authOptions: NextAuthOptions = { }, }), }, - adapter: DrizzleAdapter(db, mysqlTable) as Adapter, + adapter: DrizzleAdapter(db, createTable) as Adapter, providers: [ DiscordProvider({ clientId: env.DISCORD_CLIENT_ID, diff --git a/cli/template/extras/src/server/db/db-prisma-planetscale.ts b/cli/template/extras/src/server/db/db-prisma-planetscale.ts new file mode 100644 index 0000000000..dbb9c2ed32 --- /dev/null +++ b/cli/template/extras/src/server/db/db-prisma-planetscale.ts @@ -0,0 +1,21 @@ +import { Client } from "@planetscale/database"; +import { PrismaPlanetScale } from "@prisma/adapter-planetscale"; +import { PrismaClient } from "@prisma/client"; + +import { env } from "~/env.js"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +const client = new Client({ url: env.DATABASE_URL }); + +export const db = + globalForPrisma.prisma ?? + new PrismaClient({ + log: + env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + adapter: new PrismaPlanetScale(client), + }); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts b/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts new file mode 100644 index 0000000000..dab9d98200 --- /dev/null +++ b/cli/template/extras/src/server/db/index-drizzle/with-mysql.ts @@ -0,0 +1,12 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; + +import { env } from "~/env.js"; +import * as schema from "./schema"; + +export const db = drizzle( + mysql.createPool({ + uri: env.DATABASE_URL, + }), + { schema, mode: "default" } +); diff --git a/cli/template/extras/src/server/db/index-drizzle.ts b/cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts similarity index 100% rename from cli/template/extras/src/server/db/index-drizzle.ts rename to cli/template/extras/src/server/db/index-drizzle/with-planetscale.ts diff --git a/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts b/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts new file mode 100644 index 0000000000..28523c58e7 --- /dev/null +++ b/cli/template/extras/src/server/db/index-drizzle/with-postgres.ts @@ -0,0 +1,7 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +import { env } from "~/env.js"; +import * as schema from "./schema"; + +export const db = drizzle(postgres(env.DATABASE_URL), { schema }); diff --git a/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts b/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts new file mode 100644 index 0000000000..90e235e001 --- /dev/null +++ b/cli/template/extras/src/server/db/index-drizzle/with-sqlite.ts @@ -0,0 +1,12 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; + +import { env } from "~/env.js"; +import * as schema from "./schema"; + +export const db = drizzle( + new Database(env.DATABASE_URL, { + fileMustExist: false, + }), + { schema } +); diff --git a/cli/template/extras/src/server/db/drizzle-schema-base.ts b/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts similarity index 88% rename from cli/template/extras/src/server/db/drizzle-schema-base.ts rename to cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts index 7e50b8ee7d..bda3089138 100644 --- a/cli/template/extras/src/server/db/drizzle-schema-base.ts +++ b/cli/template/extras/src/server/db/schema-drizzle/base-mysql.ts @@ -16,9 +16,9 @@ import { * * @see https://orm.drizzle.team/docs/goodies#multi-project-schema */ -export const mysqlTable = mysqlTableCreator((name) => `project1_${name}`); +export const createTable = mysqlTableCreator((name) => `project1_${name}`); -export const posts = mysqlTable( +export const posts = createTable( "post", { id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), diff --git a/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts b/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts new file mode 100644 index 0000000000..bda3089138 --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/base-planetscale.ts @@ -0,0 +1,34 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { + bigint, + index, + mysqlTableCreator, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updatedAt").onUpdateNow(), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts b/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts new file mode 100644 index 0000000000..7e7d62a3cd --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/base-postgres.ts @@ -0,0 +1,34 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { + index, + pgTableCreator, + serial, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = pgTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: serial("id").primaryKey(), + name: varchar("name", { length: 256 }), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updatedAt"), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts b/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts new file mode 100644 index 0000000000..1615b4e07e --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/base-sqlite.ts @@ -0,0 +1,28 @@ +// Example model schema from the Drizzle docs +// https://orm.drizzle.team/docs/sql-schema-declaration + +import { sql } from "drizzle-orm"; +import { index, int, sqliteTableCreator, text } from "drizzle-orm/sqlite-core"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = sqliteTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + name: text("name", { length: 256 }), + createdAt: int("created_at", { mode: "timestamp" }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: int("updatedAt", { mode: "timestamp" }), + }, + (example) => ({ + nameIndex: index("name_idx").on(example.name), + }) +); diff --git a/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts b/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts new file mode 100644 index 0000000000..da10c0756c --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/with-auth-mysql.ts @@ -0,0 +1,118 @@ +import { relations, sql } from "drizzle-orm"; +import { + bigint, + index, + int, + mysqlTableCreator, + primaryKey, + text, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = mysqlTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + name: varchar("name", { length: 256 }), + createdById: varchar("createdById", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updatedAt").onUpdateNow(), + }, + (example) => ({ + createdByIdIdx: index("createdById_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: varchar("id", { length: 255 }).notNull().primaryKey(), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 255 }).notNull(), + emailVerified: timestamp("emailVerified", { + mode: "date", + fsp: 3, + }).default(sql`CURRENT_TIMESTAMP(3)`), + image: varchar("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), + sessions: many(sessions), +})); + +export const accounts = createTable( + "account", + { + userId: varchar("userId", { length: 255 }) + .notNull() + .references(() => users.id), + type: varchar("type", { length: 255 }) + .$type() + .notNull(), + provider: varchar("provider", { length: 255 }).notNull(), + providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: varchar("token_type", { length: 255 }), + scope: varchar("scope", { length: 255 }), + id_token: text("id_token"), + session_state: varchar("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_userId_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: varchar("sessionToken", { length: 255 }) + .notNull() + .primaryKey(), + userId: varchar("userId", { length: 255 }) + .notNull() + .references(() => users.id), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_userId_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verificationToken", + { + identifier: varchar("identifier", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/cli/template/extras/src/server/db/drizzle-schema-auth.ts b/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts similarity index 83% rename from cli/template/extras/src/server/db/drizzle-schema-auth.ts rename to cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts index b6bb842fc3..dcba89b31b 100644 --- a/cli/template/extras/src/server/db/drizzle-schema-auth.ts +++ b/cli/template/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts @@ -17,9 +17,9 @@ import { type AdapterAccount } from "next-auth/adapters"; * * @see https://orm.drizzle.team/docs/goodies#multi-project-schema */ -export const mysqlTable = mysqlTableCreator((name) => `project1_${name}`); +export const createTable = mysqlTableCreator((name) => `project1_${name}`); -export const posts = mysqlTable( +export const posts = createTable( "post", { id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), @@ -36,7 +36,7 @@ export const posts = mysqlTable( }) ); -export const users = mysqlTable("user", { +export const users = createTable("user", { id: varchar("id", { length: 255 }).notNull().primaryKey(), name: varchar("name", { length: 255 }), email: varchar("email", { length: 255 }).notNull(), @@ -52,7 +52,7 @@ export const usersRelations = relations(users, ({ many }) => ({ sessions: many(sessions), })); -export const accounts = mysqlTable( +export const accounts = createTable( "account", { userId: varchar("userId", { length: 255 }).notNull(), @@ -70,8 +70,10 @@ export const accounts = mysqlTable( session_state: varchar("session_state", { length: 255 }), }, (account) => ({ - compoundKey: primaryKey(account.provider, account.providerAccountId), - userIdIdx: index("userId_idx").on(account.userId), + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("accounts_userId_idx").on(account.userId), }) ); @@ -79,7 +81,7 @@ export const accountsRelations = relations(accounts, ({ one }) => ({ user: one(users, { fields: [accounts.userId], references: [users.id] }), })); -export const sessions = mysqlTable( +export const sessions = createTable( "session", { sessionToken: varchar("sessionToken", { length: 255 }) @@ -89,7 +91,7 @@ export const sessions = mysqlTable( expires: timestamp("expires", { mode: "date" }).notNull(), }, (session) => ({ - userIdIdx: index("userId_idx").on(session.userId), + userIdIdx: index("session_userId_idx").on(session.userId), }) ); @@ -97,7 +99,7 @@ export const sessionsRelations = relations(sessions, ({ one }) => ({ user: one(users, { fields: [sessions.userId], references: [users.id] }), })); -export const verificationTokens = mysqlTable( +export const verificationTokens = createTable( "verificationToken", { identifier: varchar("identifier", { length: 255 }).notNull(), @@ -105,6 +107,6 @@ export const verificationTokens = mysqlTable( expires: timestamp("expires", { mode: "date" }).notNull(), }, (vt) => ({ - compoundKey: primaryKey(vt.identifier, vt.token), + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), }) ); diff --git a/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts b/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts new file mode 100644 index 0000000000..7733f4f17b --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/with-auth-postgres.ts @@ -0,0 +1,116 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + integer, + pgTableCreator, + primaryKey, + serial, + text, + timestamp, + varchar, +} from "drizzle-orm/pg-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = pgTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: serial("id").primaryKey(), + name: varchar("name", { length: 256 }), + createdById: varchar("createdById", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at") + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updatedAt"), + }, + (example) => ({ + createdByIdIdx: index("createdById_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: varchar("id", { length: 255 }).notNull().primaryKey(), + name: varchar("name", { length: 255 }), + email: varchar("email", { length: 255 }).notNull(), + emailVerified: timestamp("emailVerified", { + mode: "date", + }).default(sql`CURRENT_TIMESTAMP`), + image: varchar("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); + +export const accounts = createTable( + "account", + { + userId: varchar("userId", { length: 255 }) + .notNull() + .references(() => users.id), + type: varchar("type", { length: 255 }) + .$type() + .notNull(), + provider: varchar("provider", { length: 255 }).notNull(), + providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: integer("expires_at"), + token_type: varchar("token_type", { length: 255 }), + scope: varchar("scope", { length: 255 }), + id_token: text("id_token"), + session_state: varchar("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_userId_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: varchar("sessionToken", { length: 255 }) + .notNull() + .primaryKey(), + userId: varchar("userId", { length: 255 }) + .notNull() + .references(() => users.id), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_userId_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verificationToken", + { + identifier: varchar("identifier", { length: 255 }).notNull(), + token: varchar("token", { length: 255 }).notNull(), + expires: timestamp("expires", { mode: "date" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts b/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts new file mode 100644 index 0000000000..f739573bfc --- /dev/null +++ b/cli/template/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts @@ -0,0 +1,111 @@ +import { relations, sql } from "drizzle-orm"; +import { + index, + int, + primaryKey, + sqliteTableCreator, + text, +} from "drizzle-orm/sqlite-core"; +import { type AdapterAccount } from "next-auth/adapters"; + +/** + * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same + * database instance for multiple projects. + * + * @see https://orm.drizzle.team/docs/goodies#multi-project-schema + */ +export const createTable = sqliteTableCreator((name) => `project1_${name}`); + +export const posts = createTable( + "post", + { + id: int("id", { mode: "number" }).primaryKey({ autoIncrement: true }), + name: text("name", { length: 256 }), + createdById: text("createdById", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: int("created_at", { mode: "timestamp" }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: int("updatedAt", { mode: "timestamp" }), + }, + (example) => ({ + createdByIdIdx: index("createdById_idx").on(example.createdById), + nameIndex: index("name_idx").on(example.name), + }) +); + +export const users = createTable("user", { + id: text("id", { length: 255 }).notNull().primaryKey(), + name: text("name", { length: 255 }), + email: text("email", { length: 255 }).notNull(), + emailVerified: int("emailVerified", { + mode: "timestamp", + }).default(sql`CURRENT_TIMESTAMP`), + image: text("image", { length: 255 }), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + accounts: many(accounts), +})); + +export const accounts = createTable( + "account", + { + userId: text("userId", { length: 255 }) + .notNull() + .references(() => users.id), + type: text("type", { length: 255 }) + .$type() + .notNull(), + provider: text("provider", { length: 255 }).notNull(), + providerAccountId: text("providerAccountId", { length: 255 }).notNull(), + refresh_token: text("refresh_token"), + access_token: text("access_token"), + expires_at: int("expires_at"), + token_type: text("token_type", { length: 255 }), + scope: text("scope", { length: 255 }), + id_token: text("id_token"), + session_state: text("session_state", { length: 255 }), + }, + (account) => ({ + compoundKey: primaryKey({ + columns: [account.provider, account.providerAccountId], + }), + userIdIdx: index("account_userId_idx").on(account.userId), + }) +); + +export const accountsRelations = relations(accounts, ({ one }) => ({ + user: one(users, { fields: [accounts.userId], references: [users.id] }), +})); + +export const sessions = createTable( + "session", + { + sessionToken: text("sessionToken", { length: 255 }).notNull().primaryKey(), + userId: text("userId", { length: 255 }) + .notNull() + .references(() => users.id), + expires: int("expires", { mode: "timestamp" }).notNull(), + }, + (session) => ({ + userIdIdx: index("session_userId_idx").on(session.userId), + }) +); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }), +})); + +export const verificationTokens = createTable( + "verificationToken", + { + identifier: text("identifier", { length: 255 }).notNull(), + token: text("token", { length: 255 }).notNull(), + expires: int("expires", { mode: "timestamp" }).notNull(), + }, + (vt) => ({ + compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), + }) +); diff --git a/cli/template/extras/start-database/mysql.sh b/cli/template/extras/start-database/mysql.sh new file mode 100755 index 0000000000..5319e9286e --- /dev/null +++ b/cli/template/extras/start-database/mysql.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Use this script to start a docker container for a local development database + +# TO RUN ON WINDOWS: +# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install +# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ +# 3. Open WSL - `wsl` +# 4. Run this script - `./start-database.sh` + +# On Lunux and macOS you can run this script directly - `./start-database.sh` + +DB_CONTAINER_NAME="project1-mysql" + +if ! [ -x "$(command -v docker)" ]; then + echo "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" + exit 1 +fi + +if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then + docker start $DB_CONTAINER_NAME + echo "Database container started" + exit 0 +fi + +# import env variables from .env +set -a +source .env + +DB_PASSWORD=$(echo $DATABASE_URL | awk -F':' '{print $3}' | awk -F'@' '{print $1}') + +if [ "$DB_PASSWORD" == "password" ]; then + echo "You are using the default database password" + read -p "Should we generate a random password for you? [y/N]: " -r REPLY + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Please set a password in the .env file and try again" + exit 1 + fi + DB_PASSWORD=$(openssl rand -base64 12) + sed -i -e "s/:password@/:$DB_PASSWORD@/" .env +fi + +docker run --name $DB_CONTAINER_NAME -e MYSQL_ROOT_PASSWORD=$DB_PASSWORD -e MYSQL_DATABASE=project1 -d -p 3306:3306 docker.io/mysql + +echo "Database container was succesfuly created" + + diff --git a/cli/template/extras/start-database/postgres.sh b/cli/template/extras/start-database/postgres.sh new file mode 100755 index 0000000000..f873d4df62 --- /dev/null +++ b/cli/template/extras/start-database/postgres.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Use this script to start a docker container for a local development database + +# TO RUN ON WINDOWS: +# 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install +# 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/ +# 3. Open WSL - `wsl` +# 4. Run this script - `./start-database.sh` + +# On Lunux and macOS you can run this script directly - `./start-database.sh` + +DB_CONTAINER_NAME="project1-postgres" + +if ! [ -x "$(command -v docker)" ]; then + echo "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/" + exit 1 +fi + +if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then + docker start $DB_CONTAINER_NAME + echo "Database container started" + exit 0 +fi + +# import env variables from .env +set -a +source .env + +DB_PASSWORD=$(echo $DATABASE_URL | awk -F':' '{print $3}' | awk -F'@' '{print $1}') + +if [ "$DB_PASSWORD" = "password" ]; then + echo "You are using the default database password" + read -p "Should we generate a random password for you? [y/N]: " -r REPLY + if ! [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Please set a password in the .env file and try again" + exit 1 + fi + DB_PASSWORD=$(openssl rand -base64 12) + sed -i -e "s/:password@/:$DB_PASSWORD@/" .env +fi + +docker run --name $DB_CONTAINER_NAME -e POSTGRES_PASSWORD=$DB_PASSWORD -e POSTGRES_DB=project1 -d -p 5432:5432 docker.io/postgres + +echo "Database container was succesfuly created" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 291af154fd..33a957de89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + importers: .: @@ -104,6 +108,9 @@ importers: '@planetscale/database': specifier: ^1.11.0 version: 1.11.0 + '@prisma/adapter-planetscale': + specifier: ^5.6.0 + version: 5.6.0(@planetscale/database@1.11.0) '@prisma/client': specifier: ^5.6.0 version: 5.6.0(prisma@5.6.0) @@ -2543,6 +2550,17 @@ packages: engines: {node: '>=16'} dev: true + /@prisma/adapter-planetscale@5.6.0(@planetscale/database@1.11.0): + resolution: {integrity: sha512-VhBEXKcRDbkozzo3W0mXd4lIHxvpR6YJkZRCHhPTyAx7gba7kpu/lJ83y/P1+YXRiyN2Qmf91pDVYr1qcpfCwQ==} + peerDependencies: + '@planetscale/database': ^1.11.0 + dependencies: + '@planetscale/database': 1.11.0 + '@prisma/driver-adapter-utils': 5.6.0 + transitivePeerDependencies: + - supports-color + dev: true + /@prisma/client@5.6.0(prisma@5.6.0): resolution: {integrity: sha512-mUDefQFa1wWqk4+JhKPYq8BdVoFk9NFMBXUI8jAkBfQTtgx8WPx02U2HB/XbAz3GSUJpeJOKJQtNvaAIDs6sug==} engines: {node: '>=16.13'} @@ -2557,6 +2575,14 @@ packages: prisma: 5.6.0 dev: true + /@prisma/driver-adapter-utils@5.6.0: + resolution: {integrity: sha512-/TSrfCGLAQghNf+bwg5/e8iKAgecCYU/gMN0IyNra3183/VTQJneLFgbacuSK9bBXiIRUmpbuUIrJ6dhENzfjA==} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + /@prisma/engines-version@5.6.0-32.e95e739751f42d8ca026f6b910f5a2dc5adeaeee: resolution: {integrity: sha512-UoFgbV1awGL/3wXuUK3GDaX2SolqczeeJ5b4FVec9tzeGbSWJboPSbT0psSrmgYAKiKnkOPFSLlH6+b+IyOwAw==} dev: true @@ -11061,7 +11087,3 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/tsconfig.json b/tsconfig.json index 3ff225328e..6239c83b1c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { /* LANGUAGE COMPILATION OPTIONS */ "target": "ES2020", - "lib": ["DOM", "DOM.Iterable", "ES2020"], + "lib": ["DOM", "DOM.Iterable", "ES2021"], "module": "Node16", "moduleResolution": "nodenext", "resolveJsonModule": true, diff --git a/www/src/pages/en/usage/first-steps.md b/www/src/pages/en/usage/first-steps.md index 776f3ce8ff..44e39074c4 100644 --- a/www/src/pages/en/usage/first-steps.md +++ b/www/src/pages/en/usage/first-steps.md @@ -9,6 +9,10 @@ You just scaffolded a new T3 App and are ready to go. Here is the bare minimum t ## Database +### MySQL, PostgreSQL + +If you chose MySQL or PostgreSQL as your database, your T3 app will come with a `start-database.sh` bash script that can create a docker container with a database for local development. If you already have a database, feel free to delete this file and put your database credentials in `.env`. On macOS, you can also use [DBngin](https://dbngin.com/) if you don't want to use docker. + ### Prisma If your app includes Prisma, make sure to run `npx prisma db push` from the root directory of your app. This command will sync your Prisma schema with your database and will generate the TypeScript types for the Prisma Client based on your schema. Note that you need to [restart the TypeScript server](https://tinytip.co/tips/vscode-restart-ts/) after doing this so that it can detect the generated types.