feat: add mysql database

This commit is contained in:
Aman Varshney
2025-04-09 11:54:01 +05:30
parent 3cd9f31e01
commit c9b7e25e1d
25 changed files with 607 additions and 518 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
add mysql database

View File

@@ -33,6 +33,8 @@ export const dependencyVersionMap = {
"@libsql/client": "^0.14.0",
postgres: "^3.4.5",
mysql2: "^3.14.0",
"@prisma/client": "^6.5.0",
prisma: "^6.5.0",

View File

@@ -30,58 +30,44 @@ export async function setupDatabase(
}
try {
if (databaseType === "sqlite") {
if (orm === "drizzle") {
if (orm === "prisma") {
addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
} else if (orm === "drizzle") {
if (databaseType === "sqlite") {
addPackageDependency({
dependencies: ["drizzle-orm", "@libsql/client"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else if (orm === "prisma") {
addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
}
if (setupTursoDb) {
await setupTurso(projectDir, true);
}
} else if (databaseType === "postgres") {
if (orm === "drizzle") {
} else if (databaseType === "postgres") {
addPackageDependency({
dependencies: ["drizzle-orm", "postgres"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
} else if (orm === "prisma") {
} else if (databaseType === "mysql") {
addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
projectDir: serverDir,
});
if (
databaseType === "postgres" &&
orm === "prisma" &&
setupPrismaPostgresDb
) {
await setupPrismaPostgres(projectDir, packageManager);
}
}
} else if (databaseType === "mongodb") {
if (orm === "prisma") {
addPackageDependency({
dependencies: ["@prisma/client"],
devDependencies: ["prisma"],
dependencies: ["drizzle-orm", "mysql2"],
devDependencies: ["drizzle-kit"],
projectDir: serverDir,
});
}
}
if (setupMongoDBAtlasDb) {
await setupMongoDBAtlas(projectDir);
}
if (databaseType === "sqlite" && setupTursoDb) {
await setupTurso(projectDir, orm === "drizzle");
} else if (
databaseType === "postgres" &&
orm === "prisma" &&
setupPrismaPostgresDb
) {
await setupPrismaPostgres(projectDir, packageManager);
} else if (databaseType === "mongodb" && setupMongoDBAtlasDb) {
await setupMongoDBAtlas(projectDir);
}
} catch (error) {
s.stop(pc.red("Failed to set up database"));

View File

@@ -3,73 +3,50 @@ import fs from "fs-extra";
import type { ProjectConfig } from "../types";
import { generateAuthSecret } from "./auth-setup";
interface EnvVariable {
key: string;
value: string;
condition: boolean;
}
async function addEnvVariablesToFile(
filePath: string,
variables: EnvVariable[],
): Promise<void> {
await fs.ensureDir(path.dirname(filePath));
let envContent = "";
if (await fs.pathExists(filePath)) {
envContent = await fs.readFile(filePath, "utf8");
}
let modified = false;
for (const { key, value, condition } of variables) {
if (condition) {
const regex = new RegExp(`^${key}=.*$`, "m");
if (regex.test(envContent)) {
if (value) {
envContent = envContent.replace(regex, `${key}=${value}`);
modified = true;
}
} else {
envContent += `\n${key}=${value}`;
modified = true;
}
}
}
if (modified) {
await fs.writeFile(filePath, envContent.trim());
}
}
export async function setupEnvironmentVariables(
projectDir: string,
options: ProjectConfig,
): Promise<void> {
const serverDir = path.join(projectDir, "apps/server");
const envPath = path.join(serverDir, ".env");
let envContent = "";
if (await fs.pathExists(envPath)) {
envContent = await fs.readFile(envPath, "utf8");
}
if (!envContent.includes("CORS_ORIGIN")) {
const hasReactRouter = options.frontend.includes("react-router");
const hasTanStackRouter = options.frontend.includes("tanstack-router");
const hasTanStackStart = options.frontend.includes("tanstack-start");
let corsOrigin = "http://localhost:3000";
if (hasReactRouter) {
corsOrigin = "http://localhost:5173";
} else if (hasTanStackRouter || hasTanStackStart) {
corsOrigin = "http://localhost:3001";
}
envContent += `\nCORS_ORIGIN=${corsOrigin}`;
}
if (options.auth) {
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\nBETTER_AUTH_SECRET=${generateAuthSecret()}`;
}
if (!envContent.includes("BETTER_AUTH_URL")) {
envContent += "\nBETTER_AUTH_URL=http://localhost:3000";
}
}
if (options.database !== "none") {
if (options.orm === "prisma" && !envContent.includes("DATABASE_URL")) {
let databaseUrlLine = "";
if (options.database === "sqlite") {
databaseUrlLine = "";
} else if (options.database === "postgres") {
databaseUrlLine = `\nDATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
} else if (options.database === "mongodb") {
databaseUrlLine = `\nDATABASE_URL="mongodb://localhost:27017/mydatabase"`;
}
envContent += databaseUrlLine;
}
if (options.database === "sqlite" && options.dbSetup !== "turso") {
if (!envContent.includes("DATABASE_URL")) {
envContent += "\nDATABASE_URL=file:./local.db";
}
}
}
if (
options.examples?.includes("ai") &&
!envContent.includes("GOOGLE_GENERATIVE_AI_API_KEY")
) {
envContent += "\nGOOGLE_GENERATIVE_AI_API_KEY=";
}
await fs.writeFile(envPath, envContent.trim());
const hasReactRouter = options.frontend.includes("react-router");
const hasTanStackRouter = options.frontend.includes("tanstack-router");
@@ -77,39 +54,84 @@ export async function setupEnvironmentVariables(
const hasWebFrontend =
hasReactRouter || hasTanStackRouter || hasTanStackStart;
let corsOrigin = "http://localhost:3000";
if (hasReactRouter) {
corsOrigin = "http://localhost:5173";
} else if (hasTanStackRouter || hasTanStackStart) {
corsOrigin = "http://localhost:3001";
}
let databaseUrl = "";
const specializedSetup =
options.dbSetup === "turso" ||
options.dbSetup === "prisma-postgres" ||
options.dbSetup === "mongodb-atlas";
if (!specializedSetup) {
if (options.database === "postgres") {
databaseUrl =
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public";
} else if (options.database === "mysql") {
databaseUrl = "mysql://root:password@localhost:3306/mydb";
} else if (options.database === "mongodb") {
databaseUrl = "mongodb://localhost:27017/mydatabase";
} else if (options.database === "sqlite") {
databaseUrl = "file:./local.db";
}
}
const serverVars: EnvVariable[] = [
{
key: "CORS_ORIGIN",
value: corsOrigin,
condition: true,
},
{
key: "BETTER_AUTH_SECRET",
value: generateAuthSecret(),
condition: !!options.auth,
},
{
key: "BETTER_AUTH_URL",
value: "http://localhost:3000",
condition: !!options.auth,
},
{
key: "DATABASE_URL",
value: databaseUrl,
condition:
options.database !== "none" && databaseUrl !== "" && !specializedSetup,
},
{
key: "GOOGLE_GENERATIVE_AI_API_KEY",
value: "",
condition: options.examples?.includes("ai") || false,
},
];
await addEnvVariablesToFile(envPath, serverVars);
if (hasWebFrontend) {
const clientDir = path.join(projectDir, "apps/web");
await setupClientEnvFile(clientDir);
const clientVars: EnvVariable[] = [
{
key: "VITE_SERVER_URL",
value: "http://localhost:3000",
condition: true,
},
];
await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
}
if (options.frontend.includes("native")) {
const nativeDir = path.join(projectDir, "apps/native");
const nativeEnvPath = path.join(nativeDir, ".env");
let nativeEnvContent = "";
if (await fs.pathExists(nativeEnvPath)) {
nativeEnvContent = await fs.readFile(nativeEnvPath, "utf8");
}
if (!nativeEnvContent.includes("EXPO_PUBLIC_SERVER_URL")) {
nativeEnvContent += "EXPO_PUBLIC_SERVER_URL=http://localhost:3000\n";
}
await fs.writeFile(nativeEnvPath, nativeEnvContent.trim());
const nativeVars: EnvVariable[] = [
{
key: "EXPO_PUBLIC_SERVER_URL",
value: "http://localhost:3000",
condition: true,
},
];
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
}
}
async function setupClientEnvFile(clientDir: string) {
const clientEnvPath = path.join(clientDir, ".env");
let clientEnvContent = "";
if (await fs.pathExists(clientEnvPath)) {
clientEnvContent = await fs.readFile(clientEnvPath, "utf8");
}
if (!clientEnvContent.includes("VITE_SERVER_URL")) {
clientEnvContent += "VITE_SERVER_URL=http://localhost:3000\n";
}
await fs.writeFile(clientEnvPath, clientEnvContent.trim());
}

View File

@@ -366,14 +366,15 @@ async function findGitignoreFiles(dir: string): Promise<string[]> {
function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
return database === "sqlite"
? "template/with-drizzle-sqlite"
: "template/with-drizzle-postgres";
if (database === "sqlite") return "template/with-drizzle-sqlite";
if (database === "postgres") return "template/with-drizzle-postgres";
if (database === "mysql") return "template/with-drizzle-mysql";
}
if (orm === "prisma") {
if (database === "sqlite") return "template/with-prisma-sqlite";
if (database === "postgres") return "template/with-prisma-postgres";
if (database === "mysql") return "template/with-prisma-mysql";
if (database === "mongodb") return "template/with-prisma-mongodb";
}
@@ -382,14 +383,15 @@ function getOrmTemplateDir(orm: ProjectOrm, database: ProjectDatabase): string {
function getAuthLibDir(orm: ProjectOrm, database: ProjectDatabase): string {
if (orm === "drizzle") {
return database === "sqlite"
? "with-drizzle-sqlite-lib"
: "with-drizzle-postgres-lib";
if (database === "sqlite") return "with-drizzle-sqlite-lib";
if (database === "postgres") return "with-drizzle-postgres-lib";
if (database === "mysql") return "with-drizzle-mysql-lib";
}
if (orm === "prisma") {
if (database === "sqlite") return "with-prisma-sqlite-lib";
if (database === "postgres") return "with-prisma-postgres-lib";
if (database === "mysql") return "with-prisma-mysql-lib";
if (database === "mongodb") return "with-prisma-mongodb-lib";
}

View File

@@ -40,7 +40,7 @@ async function main() {
.option("-y, --yes", "Use default configuration")
.option(
"--database <type>",
"Database type (none, sqlite, postgres, mongodb)",
"Database type (none, sqlite, postgres, mysql, mongodb)",
)
.option("--orm <type>", "ORM type (drizzle, prisma)")
.option("--auth", "Include authentication")
@@ -80,9 +80,7 @@ async function main() {
const options = program.opts() as CLIOptions;
const projectDirectory = program.args[0];
validateOptions(options);
const flagConfig = processFlags(options, projectDirectory);
const flagConfig = processAndValidateFlags(options, projectDirectory);
if (!options.yes && Object.keys(flagConfig).length > 0) {
log.info(pc.yellow("Using these pre-selected options:"));
@@ -129,41 +127,39 @@ async function main() {
}
}
function validateOptions(options: CLIOptions): void {
if (
options.database &&
!["none", "sqlite", "postgres", "mongodb"].includes(options.database)
) {
cancel(
pc.red(
`Invalid database type: ${options.database}. Must be none, sqlite, postgres, or mongodb.`,
),
);
process.exit(1);
function processAndValidateFlags(
options: CLIOptions,
projectDirectory?: string,
): Partial<ProjectConfig> {
const config: Partial<ProjectConfig> = {};
if (options.database) {
if (
!["none", "sqlite", "postgres", "mysql", "mongodb"].includes(
options.database,
)
) {
cancel(
pc.red(
`Invalid database type: ${options.database}. Must be none, sqlite, postgres, mysql, or mongodb.`,
),
);
process.exit(1);
}
config.database = options.database as ProjectDatabase;
}
if (options.orm && !["drizzle", "prisma"].includes(options.orm)) {
cancel(
pc.red(`Invalid ORM type: ${options.orm}. Must be drizzle or prisma.`),
);
process.exit(1);
if (options.orm) {
if (!["drizzle", "prisma"].includes(options.orm)) {
cancel(
pc.red(`Invalid ORM type: ${options.orm}. Must be drizzle or prisma.`),
);
process.exit(1);
}
config.orm = options.orm as ProjectOrm;
}
if (
options.dbSetup &&
!["turso", "prisma-postgres", "mongodb-atlas", "none"].includes(
options.dbSetup,
)
) {
cancel(
pc.red(
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`,
),
);
process.exit(1);
}
if (options.database === "mongodb" && options.orm === "drizzle") {
if (config.database === "mongodb" && config.orm === "drizzle") {
cancel(
pc.red(
"MongoDB is only available with Prisma. Cannot use --database mongodb with --orm drizzle",
@@ -172,7 +168,81 @@ function validateOptions(options: CLIOptions): void {
process.exit(1);
}
if (options.database === "none") {
if (options.dbSetup) {
if (
!["turso", "prisma-postgres", "mongodb-atlas", "none"].includes(
options.dbSetup,
)
) {
cancel(
pc.red(
`Invalid database setup: ${options.dbSetup}. Must be turso, prisma-postgres, mongodb-atlas, or none.`,
),
);
process.exit(1);
}
if (options.dbSetup !== "none") {
config.dbSetup = options.dbSetup as ProjectDBSetup;
if (options.dbSetup === "turso") {
if (options.database && options.database !== "sqlite") {
cancel(
pc.red(
`Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`,
),
);
process.exit(1);
}
config.database = "sqlite";
if (options.orm === "prisma") {
cancel(
pc.red(
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
),
);
process.exit(1);
}
config.orm = "drizzle";
} else if (options.dbSetup === "prisma-postgres") {
if (options.database && options.database !== "postgres") {
cancel(
pc.red(
"Prisma PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup prisma-postgres with a different database type.",
),
);
process.exit(1);
}
config.database = "postgres";
if (options.orm && options.orm !== "prisma") {
cancel(
pc.red(
"Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with a different ORM.",
),
);
process.exit(1);
}
config.orm = "prisma";
} else if (options.dbSetup === "mongodb-atlas") {
if (options.database && options.database !== "mongodb") {
cancel(
pc.red(
"MongoDB Atlas setup requires MongoDB database. Cannot use --db-setup mongodb-atlas with a different database type.",
),
);
process.exit(1);
}
config.database = "mongodb";
config.orm = "prisma";
}
} else {
config.dbSetup = "none";
}
}
if (config.database === "none") {
if (options.auth === true) {
cancel(
pc.red(
@@ -201,128 +271,30 @@ function validateOptions(options: CLIOptions): void {
}
}
if (options.dbSetup === "turso") {
if (options.database && options.database !== "sqlite") {
cancel(
pc.red(
`Turso setup requires a SQLite database. Cannot use --db-setup turso with --database ${options.database}`,
),
);
process.exit(1);
}
if (options.orm === "prisma") {
cancel(
pc.red(
"Turso setup is not compatible with Prisma. Cannot use --db-setup turso with --orm prisma",
),
);
process.exit(1);
}
if ("auth" in options) {
config.auth = options.auth;
}
if (options.dbSetup === "prisma-postgres") {
if (options.database && options.database !== "postgres") {
if (options.backend) {
if (!["hono", "elysia", "express"].includes(options.backend)) {
cancel(
pc.red(
"Prisma PostgreSQL setup requires PostgreSQL database. Cannot use --db-setup prisma-postgres with a different database type.",
),
);
process.exit(1);
}
if (options.orm && options.orm !== "prisma") {
cancel(
pc.red(
"Prisma PostgreSQL setup requires Prisma ORM. Cannot use --db-setup prisma-postgres with a different ORM.",
`Invalid backend framework: ${options.backend}. Must be hono, elysia, or express.`,
),
);
process.exit(1);
}
config.backend = options.backend as ProjectBackend;
}
if (options.dbSetup === "mongodb-atlas") {
if (options.database && options.database !== "mongodb") {
if (options.runtime) {
if (!["bun", "node"].includes(options.runtime)) {
cancel(
pc.red(
"MongoDB Atlas setup requires MongoDB database. Cannot use --db-setup mongodb-atlas with a different database type.",
),
);
process.exit(1);
}
}
if (
options.packageManager &&
!["npm", "pnpm", "bun"].includes(options.packageManager)
) {
cancel(
pc.red(
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
),
);
process.exit(1);
}
if (
options.backend &&
!["hono", "elysia", "express"].includes(options.backend)
) {
cancel(
pc.red(
`Invalid backend framework: ${options.backend}. Must be hono, elysia, or express.`,
),
);
process.exit(1);
}
if (options.runtime && !["bun", "node"].includes(options.runtime)) {
cancel(pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`));
process.exit(1);
}
if (
options.examples &&
Array.isArray(options.examples) &&
options.examples.length > 0
) {
const validExamples = ["todo", "ai"];
const invalidExamples = options.examples.filter(
(example: string) => !validExamples.includes(example),
);
if (invalidExamples.length > 0) {
cancel(
pc.red(
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
),
);
process.exit(1);
}
if (options.examples.includes("ai") && options.backend === "elysia") {
cancel(
pc.red(
"AI example is only compatible with Hono backend. Cannot use --examples ai with --backend elysia",
),
);
process.exit(1);
}
if (
options.frontend &&
!options.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start"].includes(f),
) &&
!options.frontend.includes("none")
) {
cancel(
pc.red(
"Examples require a web frontend. Cannot use --examples with --frontend native only",
),
pc.red(`Invalid runtime: ${options.runtime}. Must be bun or node.`),
);
process.exit(1);
}
config.runtime = options.runtime as ProjectRuntime;
}
if (options.frontend && options.frontend.length > 0) {
@@ -346,33 +318,42 @@ function validateOptions(options: CLIOptions): void {
process.exit(1);
}
const webFrontends = options.frontend.filter(
(f) =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start",
);
if (webFrontends.length > 1) {
cancel(
pc.red(
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router",
),
if (options.frontend.includes("none")) {
if (options.frontend.length > 1) {
cancel(pc.red(`Cannot combine 'none' with other frontend options.`));
process.exit(1);
}
config.frontend = [];
} else {
const validOptions = options.frontend.filter(
(f): f is ProjectFrontend =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start" ||
f === "native",
);
process.exit(1);
}
if (options.frontend.includes("none") && options.frontend.length > 1) {
cancel(pc.red(`Cannot combine 'none' with other frontend options.`));
process.exit(1);
const webFrontends = validOptions.filter(
(f) =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start",
);
if (webFrontends.length > 1) {
cancel(
pc.red(
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router",
),
);
process.exit(1);
}
config.frontend = validOptions;
}
}
if (
options.addons &&
Array.isArray(options.addons) &&
options.addons.length > 0
) {
if (options.addons && options.addons.length > 0) {
const validAddons = ["pwa", "tauri", "biome", "husky", "none"];
const invalidAddons = options.addons.filter(
(addon: string) => !validAddons.includes(addon),
@@ -387,108 +368,14 @@ function validateOptions(options: CLIOptions): void {
process.exit(1);
}
if (options.addons.includes("none") && options.addons.length > 1) {
cancel(pc.red(`Cannot combine 'none' with other addons.`));
process.exit(1);
}
const webSpecificAddons = ["pwa", "tauri"];
const hasWebSpecificAddons = options.addons.some((addon) =>
webSpecificAddons.includes(addon),
);
if (
hasWebSpecificAddons &&
options.frontend &&
!options.frontend.some((f) =>
["tanstack-router", "react-router"].includes(f),
)
) {
cancel(
pc.red(
`PWA and Tauri addons require tanstack-router or react-router. Cannot use --addons ${options.addons
.filter((a) => webSpecificAddons.includes(a))
.join(", ")} with incompatible frontend options.`,
),
);
process.exit(1);
}
}
}
function processFlags(
options: CLIOptions,
projectDirectory?: string,
): Partial<ProjectConfig> {
let frontend: ProjectFrontend[] | undefined = undefined;
if (options.frontend) {
if (options.frontend.includes("none")) {
frontend = [];
} else {
frontend = options.frontend.filter(
(f): f is ProjectFrontend =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start" ||
f === "native",
);
const webFrontends = frontend.filter(
(f) =>
f === "tanstack-router" ||
f === "react-router" ||
f === "tanstack-start",
);
if (webFrontends.length > 1) {
const firstWebFrontend = webFrontends[0];
frontend = frontend.filter(
(f) => f === "native" || f === firstWebFrontend,
);
}
}
}
let examples: ProjectExamples[] | undefined;
if ("examples" in options) {
if (options.examples === false) {
examples = [];
} else if (Array.isArray(options.examples)) {
examples = options.examples.filter(
(ex): ex is ProjectExamples => ex === "todo" || ex === "ai",
);
if (
frontend &&
frontend.length > 0 &&
!frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start"].includes(f),
)
) {
examples = [];
log.warn(
pc.yellow("Examples require web frontend - ignoring examples flag"),
);
}
if (examples.includes("ai") && options.backend === "elysia") {
examples = examples.filter((ex) => ex !== "ai");
log.warn(
pc.yellow(
"AI example is not compatible with Elysia - removing AI example",
),
);
}
}
}
let addons: ProjectAddons[] | undefined;
if (options.addons && Array.isArray(options.addons)) {
if (options.addons.includes("none")) {
addons = [];
if (options.addons.length > 1) {
cancel(pc.red(`Cannot combine 'none' with other addons.`));
process.exit(1);
}
config.addons = [];
} else {
addons = options.addons.filter(
const validOptions = options.addons.filter(
(addon): addon is ProjectAddons =>
addon === "pwa" ||
addon === "tauri" ||
@@ -496,85 +383,111 @@ function processFlags(
addon === "husky",
);
const hasCompatibleWebFrontend = frontend?.some(
const webSpecificAddons = ["pwa", "tauri"];
const hasWebSpecificAddons = validOptions.some((addon) =>
webSpecificAddons.includes(addon),
);
const hasCompatibleWebFrontend = config.frontend?.some(
(f) => f === "tanstack-router" || f === "react-router",
);
if (!hasCompatibleWebFrontend) {
const webSpecificAddons = ["pwa", "tauri"];
const filteredAddons = addons.filter(
(addon) => !webSpecificAddons.includes(addon),
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
cancel(
pc.red(
"PWA and Tauri addons require tanstack-router or react-router. Cannot use these addons with your frontend selection.",
),
);
if (filteredAddons.length !== addons.length) {
log.warn(
pc.yellow(
"PWA and Tauri addons require tanstack-router or react-router - removing these addons",
),
);
addons = filteredAddons;
}
process.exit(1);
}
if (addons.includes("husky") && !addons.includes("biome")) {
addons.push("biome");
if (validOptions.includes("husky") && !validOptions.includes("biome")) {
validOptions.push("biome");
}
config.addons = validOptions;
}
}
let database = options.database as ProjectDatabase | undefined;
let orm = options.orm as ProjectOrm | undefined;
const auth = "auth" in options ? options.auth : undefined;
if ("examples" in options) {
if (options.examples === false) {
config.examples = [];
} else if (Array.isArray(options.examples)) {
const validExamples = ["todo", "ai"];
const invalidExamples = options.examples.filter(
(example: string) => !validExamples.includes(example),
);
const backend = options.backend as ProjectBackend | undefined;
const runtime = options.runtime as ProjectRuntime | undefined;
const packageManager = options.packageManager as
| ProjectPackageManager
| undefined;
let dbSetup: ProjectDBSetup | undefined = undefined;
if (options.dbSetup) {
if (options.dbSetup === "none") {
dbSetup = "none";
} else {
dbSetup = options.dbSetup as ProjectDBSetup;
if (dbSetup === "turso") {
database = "sqlite";
if (orm === "prisma") {
log.warn(
pc.yellow(
"Turso is not compatible with Prisma - switching to Drizzle",
),
);
orm = "drizzle";
}
} else if (dbSetup === "prisma-postgres") {
database = "postgres";
orm = "prisma";
} else if (dbSetup === "mongodb-atlas") {
database = "mongodb";
orm = "prisma";
if (invalidExamples.length > 0) {
cancel(
pc.red(
`Invalid example(s): ${invalidExamples.join(", ")}. Valid options are: ${validExamples.join(", ")}.`,
),
);
process.exit(1);
}
if (
options.examples.includes("ai") &&
(options.backend === "elysia" || config.backend === "elysia")
) {
cancel(
pc.red(
"AI example is only compatible with Hono backend. Cannot use --examples ai with --backend elysia",
),
);
process.exit(1);
}
const hasWebFrontend = config.frontend?.some((f) =>
["tanstack-router", "react-router", "tanstack-start"].includes(f),
);
if (
options.examples.length > 0 &&
!hasWebFrontend &&
(!options.frontend ||
!options.frontend.some((f) =>
["tanstack-router", "react-router", "tanstack-start"].includes(f),
))
) {
cancel(
pc.red(
"Examples require a web frontend (tanstack-router, react-router, or tanstack-start). Cannot use --examples without a compatible frontend.",
),
);
process.exit(1);
}
config.examples = options.examples.filter(
(ex): ex is ProjectExamples => ex === "todo" || ex === "ai",
);
}
}
const config: Partial<ProjectConfig> = {};
if (options.packageManager) {
if (!["npm", "pnpm", "bun"].includes(options.packageManager)) {
cancel(
pc.red(
`Invalid package manager: ${options.packageManager}. Must be npm, pnpm, or bun.`,
),
);
process.exit(1);
}
config.packageManager = options.packageManager as ProjectPackageManager;
}
if (projectDirectory) config.projectName = projectDirectory;
if (database !== undefined) config.database = database;
if (orm !== undefined) config.orm = orm;
if (auth !== undefined) config.auth = auth;
if (packageManager) config.packageManager = packageManager;
if ("git" in options) config.git = options.git;
if ("install" in options) config.noInstall = !options.install;
if (dbSetup !== undefined) config.dbSetup = dbSetup;
if (backend) config.backend = backend;
if (runtime) config.runtime = runtime;
if (frontend !== undefined) config.frontend = frontend;
if (addons !== undefined) config.addons = addons;
if (examples !== undefined) config.examples = examples;
if ("git" in options) {
config.git = options.git;
}
if ("install" in options) {
config.noInstall = !options.install;
}
if (projectDirectory) {
config.projectName = projectDirectory;
}
return config;
}

View File

@@ -45,27 +45,6 @@ type PromptGroupResults = {
export async function gatherConfig(
flags: Partial<ProjectConfig>,
): Promise<ProjectConfig> {
if (flags.dbSetup) {
if (flags.dbSetup === "turso") {
flags.database = "sqlite";
if (flags.orm === "prisma") {
log.warn(
pc.yellow(
"Turso is not compatible with Prisma - switching to Drizzle",
),
);
flags.orm = "drizzle";
}
} else if (flags.dbSetup === "prisma-postgres") {
flags.database = "postgres";
flags.orm = "prisma";
} else if (flags.dbSetup === "mongodb-atlas") {
flags.database = "mongodb";
flags.orm = "prisma";
}
}
const result = await group<PromptGroupResults>(
{
projectName: async () => {
@@ -84,7 +63,11 @@ export async function gatherConfig(
results.frontend,
),
dbSetup: ({ results }) =>
getDBSetupChoice(results.database ?? "none", flags.dbSetup),
getDBSetupChoice(
results.database ?? "none",
flags.dbSetup,
results.orm,
),
addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend),
examples: ({ results }) =>
getExamplesChoice(

View File

@@ -26,6 +26,11 @@ export async function getDatabaseChoice(
label: "PostgreSQL",
hint: "powerful, open source object-relational database system",
},
{
value: "mysql",
label: "MySQL",
hint: "popular open-source relational database system",
},
{
value: "mongodb",
label: "MongoDB",

View File

@@ -1,13 +1,18 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import type { ProjectDBSetup } from "../types";
import type { ProjectDBSetup, ProjectOrm } from "../types";
export async function getDBSetupChoice(
databaseType: string,
dbSetup: ProjectDBSetup | undefined,
orm?: ProjectOrm,
): Promise<ProjectDBSetup> {
if (dbSetup !== undefined) return dbSetup as ProjectDBSetup;
if (databaseType === "sqlite" && orm === "prisma") {
return "none";
}
let options: Array<{ value: ProjectDBSetup; label: string; hint: string }> =
[];
@@ -16,7 +21,7 @@ export async function getDBSetupChoice(
{
value: "turso" as const,
label: "Turso",
hint: "SQLite for Production. Powered by libSQL.",
hint: "SQLite for Production. Powered by libSQL",
},
{ value: "none" as const, label: "None", hint: "Manual setup" },
];

View File

@@ -1,4 +1,9 @@
export type ProjectDatabase = "sqlite" | "postgres" | "mongodb" | "none";
export type ProjectDatabase =
| "sqlite"
| "postgres"
| "mongodb"
| "mysql"
| "none";
export type ProjectOrm = "drizzle" | "prisma" | "none";
export type ProjectPackageManager = "npm" | "pnpm" | "bun";
export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";

View File

@@ -18,10 +18,12 @@
"persistent": true
},
"db:push": {
"cache": false
"cache": false,
"persistent": true
},
"db:studio": {
"cache": false
"cache": false,
"persistent": true
}
}
}

View File

@@ -12,12 +12,9 @@ export const todoRouter = router({
create: publicProcedure
.input(z.object({ text: z.string().min(1) }))
.mutation(async ({ input }) => {
return await db
.insert(todo)
.values({
text: input.text,
})
.returning();
return await db.insert(todo).values({
text: input.text,
});
}),
toggle: publicProcedure
@@ -26,16 +23,12 @@ export const todoRouter = router({
return await db
.update(todo)
.set({ completed: input.completed })
.where(eq(todo.id, input.id))
.returning();
.where(eq(todo.id, input.id));
}),
delete: publicProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input }) => {
return await db
.delete(todo)
.where(eq(todo.id, input.id))
.returning();
return await db.delete(todo).where(eq(todo.id, input.id));
}),
});

View File

@@ -0,0 +1,15 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema/auth";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "mysql",
schema: schema,
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: {
enabled: true,
},
});

View File

@@ -8,10 +8,4 @@ export const auth = betterAuth({
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true },
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
},
},
});

View File

@@ -0,0 +1,11 @@
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import prisma from "../../prisma";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "mysql",
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true },
});

View File

@@ -8,10 +8,4 @@ export const auth = betterAuth({
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true },
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
},
},
});

View File

@@ -8,10 +8,4 @@ export const auth = betterAuth({
}),
trustedOrigins: [process.env.CORS_ORIGIN || ""],
emailAndPassword: { enabled: true },
advanced: {
defaultCookieAttributes: {
sameSite: "none",
secure: true,
},
},
});

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema",
out: "./src/db/migrations",
dialect: "mysql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});

View File

@@ -0,0 +1,3 @@
import { drizzle } from "drizzle-orm/mysql2";
export const db = drizzle({ connection: { uri: process.env.DATABASE_URL } });

View File

@@ -0,0 +1,58 @@
import {
mysqlTable,
varchar,
text,
timestamp,
boolean,
} from "drizzle-orm/mysql-core";
export const user = mysqlTable("user", {
id: varchar("id", { length: 36 }).primaryKey(),
name: text("name").notNull(),
email: varchar("email", { length: 255 }).notNull().unique(),
emailVerified: boolean("email_verified").notNull(),
image: text("image"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
export const session = mysqlTable("session", {
id: varchar("id", { length: 36 }).primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: varchar("token", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const account = mysqlTable("account", {
id: varchar("id", { length: 36 }).primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: varchar("user_id", { length: 36 })
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").notNull(),
updatedAt: timestamp("updated_at").notNull(),
});
export const verification = mysqlTable("verification", {
id: varchar("id", { length: 36 }).primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at"),
updatedAt: timestamp("updated_at"),
});

View File

@@ -0,0 +1,7 @@
import { mysqlTable, varchar, int, boolean } from "drizzle-orm/mysql-core";
export const todo = mysqlTable("todo", {
id: int("id").primaryKey().autoincrement(),
text: varchar("text", { length: 255 }).notNull(),
completed: boolean("completed").default(false).notNull(),
});

View File

@@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client";
let prisma = new PrismaClient();
export default prisma;

View File

@@ -0,0 +1,59 @@
model User {
id String @id
name String @db.Text
email String
emailVerified Boolean
image String? @db.Text
createdAt DateTime
updatedAt DateTime
sessions Session[]
accounts Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id
expiresAt DateTime
token String
createdAt DateTime
updatedAt DateTime
ipAddress String? @db.Text
userAgent String? @db.Text
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([token])
@@map("session")
}
model Account {
id String @id
accountId String @db.Text
providerId String @db.Text
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String? @db.Text
refreshToken String? @db.Text
idToken String? @db.Text
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String? @db.Text
password String? @db.Text
createdAt DateTime
updatedAt DateTime
@@map("account")
}
model Verification {
id String @id
identifier String @db.Text
value String @db.Text
expiresAt DateTime
createdAt DateTime?
updatedAt DateTime?
@@map("verification")
}

View File

@@ -0,0 +1,9 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["prismaSchemaFolder"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}

View File

@@ -0,0 +1,7 @@
model Todo {
id Int @id @default(autoincrement())
text String
completed Boolean @default(false)
@@map("todo")
}