mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
Add D1 Database (#335)
This commit is contained in:
5
.changeset/eager-zebras-jog.md
Normal file
5
.changeset/eager-zebras-jog.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"create-better-t-stack": minor
|
||||
---
|
||||
|
||||
add d1 database setup
|
||||
34
apps/cli/src/helpers/database-providers/d1-setup.ts
Normal file
34
apps/cli/src/helpers/database-providers/d1-setup.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import path from "node:path";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import {
|
||||
addEnvVariablesToFile,
|
||||
type EnvVariable,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
export async function setupCloudflareD1(config: ProjectConfig): Promise<void> {
|
||||
const { projectDir } = config;
|
||||
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "CLOUDFLARE_ACCOUNT_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_DATABASE_ID",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "CLOUDFLARE_D1_TOKEN",
|
||||
value: "",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
} catch (_err) {}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import { setupAuth } from "../setup/auth-setup";
|
||||
import { setupBackendDependencies } from "../setup/backend-setup";
|
||||
import { setupDatabase } from "../setup/db-setup";
|
||||
import { setupExamples } from "../setup/examples-setup";
|
||||
import { setupRuntime } from "../setup/runtime-setup";
|
||||
import {
|
||||
generateCloudflareWorkerTypes,
|
||||
setupRuntime,
|
||||
} from "../setup/runtime-setup";
|
||||
import { createReadme } from "./create-readme";
|
||||
import { setupEnvironmentVariables } from "./env-setup";
|
||||
import { installDependencies } from "./install-dependencies";
|
||||
@@ -64,6 +67,7 @@ export async function createProject(options: ProjectConfig) {
|
||||
}
|
||||
|
||||
await handleExtras(projectDir, options);
|
||||
|
||||
await setupEnvironmentVariables(options);
|
||||
await updatePackageConfigurations(projectDir, options);
|
||||
await createReadme(projectDir, options);
|
||||
@@ -76,6 +80,7 @@ export async function createProject(options: ProjectConfig) {
|
||||
projectDir,
|
||||
packageManager: options.packageManager,
|
||||
});
|
||||
await generateCloudflareWorkerTypes(options);
|
||||
}
|
||||
|
||||
displayPostInstallInstructions({
|
||||
|
||||
@@ -186,7 +186,8 @@ export async function setupEnvironmentVariables(
|
||||
dbSetup === "prisma-postgres" ||
|
||||
dbSetup === "mongodb-atlas" ||
|
||||
dbSetup === "neon" ||
|
||||
dbSetup === "supabase";
|
||||
dbSetup === "supabase" ||
|
||||
dbSetup === "d1";
|
||||
|
||||
if (database !== "none" && !specializedSetup) {
|
||||
switch (database) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { consola } from "consola";
|
||||
import pc from "picocolors";
|
||||
import type { Database, ORM, ProjectConfig, Runtime } from "../../types";
|
||||
import type {
|
||||
Database,
|
||||
DatabaseSetup,
|
||||
ORM,
|
||||
ProjectConfig,
|
||||
Runtime,
|
||||
} from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
|
||||
export function displayPostInstallInstructions(
|
||||
@@ -16,6 +22,7 @@ export function displayPostInstallInstructions(
|
||||
runtime,
|
||||
frontend,
|
||||
backend,
|
||||
dbSetup,
|
||||
} = config;
|
||||
|
||||
const isConvex = backend === "convex";
|
||||
@@ -26,7 +33,7 @@ export function displayPostInstallInstructions(
|
||||
|
||||
const databaseInstructions =
|
||||
!isConvex && database !== "none"
|
||||
? getDatabaseInstructions(database, orm, runCmd, runtime)
|
||||
? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
||||
: "";
|
||||
|
||||
const tauriInstructions = addons?.includes("tauri")
|
||||
@@ -96,6 +103,11 @@ export function displayPostInstallInstructions(
|
||||
}
|
||||
|
||||
if (runtime === "workers") {
|
||||
if (dbSetup === "d1") {
|
||||
output += `${pc.yellow(
|
||||
"IMPORTANT:",
|
||||
)} Complete D1 database setup first (see Database commands below)\n`;
|
||||
}
|
||||
output += `${pc.cyan(`${stepCounter++}.`)} bun dev\n`;
|
||||
output += `${pc.cyan(
|
||||
`${stepCounter++}.`,
|
||||
@@ -178,9 +190,46 @@ function getDatabaseInstructions(
|
||||
orm?: ORM,
|
||||
runCmd?: string,
|
||||
runtime?: Runtime,
|
||||
dbSetup?: DatabaseSetup,
|
||||
): string {
|
||||
const instructions = [];
|
||||
|
||||
if (runtime === "workers" && dbSetup === "d1") {
|
||||
const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
|
||||
|
||||
instructions.push(
|
||||
`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(
|
||||
`${packageManager} wrangler login`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("2.")} Create D1 database: ${pc.white(
|
||||
`${packageManager} wrangler d1 create your-database-name`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan(
|
||||
"3.",
|
||||
)} Update apps/server/wrangler.jsonc with database_id and database_name`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("4.")} Generate migrations: ${pc.white(
|
||||
"cd apps/server && bun db:generate",
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("5.")} Apply migrations locally: ${pc.white(
|
||||
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push(
|
||||
`${pc.cyan("6.")} Apply migrations to production: ${pc.white(
|
||||
`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`,
|
||||
)}`,
|
||||
);
|
||||
instructions.push("");
|
||||
}
|
||||
|
||||
if (orm === "prisma") {
|
||||
if (database === "sqlite") {
|
||||
instructions.push(
|
||||
@@ -204,7 +253,7 @@ function getDatabaseInstructions(
|
||||
} else if (orm === "drizzle") {
|
||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
||||
if (database === "sqlite") {
|
||||
if (database === "sqlite" && dbSetup !== "d1") {
|
||||
instructions.push(
|
||||
`${pc.cyan(
|
||||
"•",
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { setupCloudflareD1 } from "../database-providers/d1-setup";
|
||||
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
||||
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
||||
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup";
|
||||
@@ -69,6 +70,8 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
|
||||
|
||||
if (database === "sqlite" && dbSetup === "turso") {
|
||||
await setupTurso(config);
|
||||
} else if (database === "sqlite" && dbSetup === "d1") {
|
||||
await setupCloudflareD1(config);
|
||||
} else if (database === "postgres") {
|
||||
if (orm === "prisma" && dbSetup === "prisma-postgres") {
|
||||
await setupPrismaPostgres(config);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import path from "node:path";
|
||||
import { spinner } from "@clack/prompts";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { Backend, ProjectConfig } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
|
||||
@@ -25,6 +28,43 @@ export async function setupRuntime(config: ProjectConfig): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateCloudflareWorkerTypes(
|
||||
config: ProjectConfig,
|
||||
): Promise<void> {
|
||||
if (config.runtime !== "workers") {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverDir = path.join(config.projectDir, "apps/server");
|
||||
|
||||
if (!(await fs.pathExists(serverDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start("Generating Cloudflare Workers types...");
|
||||
|
||||
const runCmd =
|
||||
config.packageManager === "npm" ? "npm" : config.packageManager;
|
||||
await execa(runCmd, ["run", "cf-typegen"], {
|
||||
cwd: serverDir,
|
||||
});
|
||||
|
||||
s.stop("Cloudflare Workers types generated successfully!");
|
||||
} catch {
|
||||
s.stop(pc.yellow("Failed to generate Cloudflare Workers types"));
|
||||
const managerCmd =
|
||||
config.packageManager === "npm"
|
||||
? "npm run"
|
||||
: `${config.packageManager} run`;
|
||||
console.warn(
|
||||
`Note: You can manually run 'cd apps/server && ${managerCmd} cf-typegen' in the project directory later`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBunRuntime(
|
||||
serverDir: string,
|
||||
_backend: Backend,
|
||||
|
||||
@@ -85,6 +85,7 @@ export async function gatherConfig(
|
||||
flags.dbSetup,
|
||||
results.orm,
|
||||
results.backend,
|
||||
results.runtime,
|
||||
),
|
||||
git: () => getGitChoice(flags.git),
|
||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { cancel, isCancel, select } from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import type { Backend, DatabaseSetup, ORM } from "../types";
|
||||
import type { Backend, DatabaseSetup, ORM, Runtime } from "../types";
|
||||
|
||||
export async function getDBSetupChoice(
|
||||
databaseType: string,
|
||||
dbSetup: DatabaseSetup | undefined,
|
||||
orm?: ORM,
|
||||
backend?: Backend,
|
||||
runtime?: Runtime,
|
||||
): Promise<DatabaseSetup> {
|
||||
if (backend === "convex") {
|
||||
return "none";
|
||||
@@ -32,6 +33,15 @@ export async function getDBSetupChoice(
|
||||
label: "Turso",
|
||||
hint: "SQLite for Production. Powered by libSQL",
|
||||
},
|
||||
...(runtime === "workers"
|
||||
? [
|
||||
{
|
||||
value: "d1" as const,
|
||||
label: "Cloudflare D1",
|
||||
hint: "Cloudflare's managed, serverless database with SQLite's SQL semantics",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
||||
];
|
||||
} else if (databaseType === "postgres") {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const DatabaseSetupSchema = z
|
||||
"prisma-postgres",
|
||||
"mongodb-atlas",
|
||||
"supabase",
|
||||
"d1",
|
||||
"none",
|
||||
])
|
||||
.describe("Database hosting setup");
|
||||
|
||||
@@ -358,6 +358,22 @@ export function processAndValidateFlags(
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.dbSetup === "d1") {
|
||||
if (config.database !== "sqlite") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.runtime !== "workers") {
|
||||
consola.fatal(
|
||||
"Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
providedFlags.has("runtime") &&
|
||||
options.runtime === "workers" &&
|
||||
|
||||
@@ -3,6 +3,16 @@ import { defineConfig } from "drizzle-kit";
|
||||
export default defineConfig({
|
||||
schema: "./src/db/schema",
|
||||
out: "./src/db/migrations",
|
||||
{{#if (eq dbSetup "d1")}}
|
||||
// DOCS: https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit
|
||||
dialect: "sqlite",
|
||||
driver: "d1-http",
|
||||
dbCredentials: {
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
|
||||
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
|
||||
token: process.env.CLOUDFLARE_D1_TOKEN!,
|
||||
},
|
||||
{{else}}
|
||||
dialect: "turso",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL || "",
|
||||
@@ -10,4 +20,5 @@ export default defineConfig({
|
||||
authToken: process.env.DATABASE_AUTH_TOKEN,
|
||||
{{/if}}
|
||||
},
|
||||
{{/if}}
|
||||
});
|
||||
|
||||
@@ -13,6 +13,12 @@ export const db = drizzle({ client });
|
||||
{{/if}}
|
||||
|
||||
{{#if (eq runtime "workers")}}
|
||||
{{#if (eq dbSetup "d1")}}
|
||||
import { drizzle } from "drizzle-orm/d1";
|
||||
import { env } from "cloudflare:workers";
|
||||
|
||||
export const db = drizzle(env.DB);
|
||||
{{else}}
|
||||
import { drizzle } from "drizzle-orm/libsql";
|
||||
import { env } from "cloudflare:workers";
|
||||
import { createClient } from "@libsql/client";
|
||||
@@ -26,3 +32,4 @@ const client = createClient({
|
||||
|
||||
export const db = drizzle({ client });
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -5,14 +5,30 @@
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"vars": {
|
||||
"NODE_ENV": "production"
|
||||
// Non-sensitive environment variables (visible in dashboard)
|
||||
// "CORS_ORIGIN": "https://your-frontend-domain.com",
|
||||
// "BETTER_AUTH_URL": "https://your-worker-domain.workers.dev"
|
||||
// Add public environment variables here
|
||||
// Example: "CORS_ORIGIN": "https://your-domain.com"
|
||||
}
|
||||
// ⚠️ SENSITIVE DATA: Use `wrangler secret put` instead of adding here
|
||||
// Don't put these in "vars" - they'll be visible in the dashboard!
|
||||
// - DATABASE_URL
|
||||
// - DATABASE_AUTH_TOKEN
|
||||
// - GOOGLE_GENERATIVE_AI_API_KEY
|
||||
// - BETTER_AUTH_SECRET
|
||||
// For sensitive data, use:
|
||||
// wrangler secret put SECRET_NAME
|
||||
// Don't add secrets to "vars" - they're visible in the dashboard!
|
||||
|
||||
{{#if (eq dbSetup "d1")}},
|
||||
// To set up D1 database:
|
||||
// 1. Run: wrangler login
|
||||
// 2. Run: wrangler d1 create your-database-name
|
||||
// 3. Copy the output and paste below
|
||||
// Then run migrations:
|
||||
// bun db:generate
|
||||
// To apply migrations locally, run:
|
||||
// wrangler d1 migrations apply YOUR_DB_NAME --local
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "YOUR_DB_NAME",
|
||||
"database_id": "YOUR_DB_ID",
|
||||
"preview_database_id": "local-test-db",
|
||||
"migrations_dir": "./src/db/migrations"
|
||||
}
|
||||
]
|
||||
{{/if}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user