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 { setupBackendDependencies } from "../setup/backend-setup";
|
||||||
import { setupDatabase } from "../setup/db-setup";
|
import { setupDatabase } from "../setup/db-setup";
|
||||||
import { setupExamples } from "../setup/examples-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 { createReadme } from "./create-readme";
|
||||||
import { setupEnvironmentVariables } from "./env-setup";
|
import { setupEnvironmentVariables } from "./env-setup";
|
||||||
import { installDependencies } from "./install-dependencies";
|
import { installDependencies } from "./install-dependencies";
|
||||||
@@ -64,6 +67,7 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await handleExtras(projectDir, options);
|
await handleExtras(projectDir, options);
|
||||||
|
|
||||||
await setupEnvironmentVariables(options);
|
await setupEnvironmentVariables(options);
|
||||||
await updatePackageConfigurations(projectDir, options);
|
await updatePackageConfigurations(projectDir, options);
|
||||||
await createReadme(projectDir, options);
|
await createReadme(projectDir, options);
|
||||||
@@ -76,6 +80,7 @@ export async function createProject(options: ProjectConfig) {
|
|||||||
projectDir,
|
projectDir,
|
||||||
packageManager: options.packageManager,
|
packageManager: options.packageManager,
|
||||||
});
|
});
|
||||||
|
await generateCloudflareWorkerTypes(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
displayPostInstallInstructions({
|
displayPostInstallInstructions({
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ export async function setupEnvironmentVariables(
|
|||||||
dbSetup === "prisma-postgres" ||
|
dbSetup === "prisma-postgres" ||
|
||||||
dbSetup === "mongodb-atlas" ||
|
dbSetup === "mongodb-atlas" ||
|
||||||
dbSetup === "neon" ||
|
dbSetup === "neon" ||
|
||||||
dbSetup === "supabase";
|
dbSetup === "supabase" ||
|
||||||
|
dbSetup === "d1";
|
||||||
|
|
||||||
if (database !== "none" && !specializedSetup) {
|
if (database !== "none" && !specializedSetup) {
|
||||||
switch (database) {
|
switch (database) {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { consola } from "consola";
|
import { consola } from "consola";
|
||||||
import pc from "picocolors";
|
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";
|
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||||
|
|
||||||
export function displayPostInstallInstructions(
|
export function displayPostInstallInstructions(
|
||||||
@@ -16,6 +22,7 @@ export function displayPostInstallInstructions(
|
|||||||
runtime,
|
runtime,
|
||||||
frontend,
|
frontend,
|
||||||
backend,
|
backend,
|
||||||
|
dbSetup,
|
||||||
} = config;
|
} = config;
|
||||||
|
|
||||||
const isConvex = backend === "convex";
|
const isConvex = backend === "convex";
|
||||||
@@ -26,7 +33,7 @@ export function displayPostInstallInstructions(
|
|||||||
|
|
||||||
const databaseInstructions =
|
const databaseInstructions =
|
||||||
!isConvex && database !== "none"
|
!isConvex && database !== "none"
|
||||||
? getDatabaseInstructions(database, orm, runCmd, runtime)
|
? getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const tauriInstructions = addons?.includes("tauri")
|
const tauriInstructions = addons?.includes("tauri")
|
||||||
@@ -96,6 +103,11 @@ export function displayPostInstallInstructions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (runtime === "workers") {
|
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++}.`)} bun dev\n`;
|
||||||
output += `${pc.cyan(
|
output += `${pc.cyan(
|
||||||
`${stepCounter++}.`,
|
`${stepCounter++}.`,
|
||||||
@@ -178,9 +190,46 @@ function getDatabaseInstructions(
|
|||||||
orm?: ORM,
|
orm?: ORM,
|
||||||
runCmd?: string,
|
runCmd?: string,
|
||||||
runtime?: Runtime,
|
runtime?: Runtime,
|
||||||
|
dbSetup?: DatabaseSetup,
|
||||||
): string {
|
): string {
|
||||||
const instructions = [];
|
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 (orm === "prisma") {
|
||||||
if (database === "sqlite") {
|
if (database === "sqlite") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
@@ -204,7 +253,7 @@ function getDatabaseInstructions(
|
|||||||
} else if (orm === "drizzle") {
|
} else if (orm === "drizzle") {
|
||||||
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
|
||||||
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
|
||||||
if (database === "sqlite") {
|
if (database === "sqlite" && dbSetup !== "d1") {
|
||||||
instructions.push(
|
instructions.push(
|
||||||
`${pc.cyan(
|
`${pc.cyan(
|
||||||
"•",
|
"•",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import fs from "fs-extra";
|
|||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { ProjectConfig } from "../../types";
|
import type { ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||||
|
import { setupCloudflareD1 } from "../database-providers/d1-setup";
|
||||||
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup";
|
||||||
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
import { setupNeonPostgres } from "../database-providers/neon-setup";
|
||||||
import { setupPrismaPostgres } from "../database-providers/prisma-postgres-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") {
|
if (database === "sqlite" && dbSetup === "turso") {
|
||||||
await setupTurso(config);
|
await setupTurso(config);
|
||||||
|
} else if (database === "sqlite" && dbSetup === "d1") {
|
||||||
|
await setupCloudflareD1(config);
|
||||||
} else if (database === "postgres") {
|
} else if (database === "postgres") {
|
||||||
if (orm === "prisma" && dbSetup === "prisma-postgres") {
|
if (orm === "prisma" && dbSetup === "prisma-postgres") {
|
||||||
await setupPrismaPostgres(config);
|
await setupPrismaPostgres(config);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { spinner } from "@clack/prompts";
|
||||||
|
import { execa } from "execa";
|
||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
|
import pc from "picocolors";
|
||||||
import type { Backend, ProjectConfig } from "../../types";
|
import type { Backend, ProjectConfig } from "../../types";
|
||||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
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(
|
async function setupBunRuntime(
|
||||||
serverDir: string,
|
serverDir: string,
|
||||||
_backend: Backend,
|
_backend: Backend,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export async function gatherConfig(
|
|||||||
flags.dbSetup,
|
flags.dbSetup,
|
||||||
results.orm,
|
results.orm,
|
||||||
results.backend,
|
results.backend,
|
||||||
|
results.runtime,
|
||||||
),
|
),
|
||||||
git: () => getGitChoice(flags.git),
|
git: () => getGitChoice(flags.git),
|
||||||
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { cancel, isCancel, select } from "@clack/prompts";
|
import { cancel, isCancel, select } from "@clack/prompts";
|
||||||
import pc from "picocolors";
|
import pc from "picocolors";
|
||||||
import type { Backend, DatabaseSetup, ORM } from "../types";
|
import type { Backend, DatabaseSetup, ORM, Runtime } from "../types";
|
||||||
|
|
||||||
export async function getDBSetupChoice(
|
export async function getDBSetupChoice(
|
||||||
databaseType: string,
|
databaseType: string,
|
||||||
dbSetup: DatabaseSetup | undefined,
|
dbSetup: DatabaseSetup | undefined,
|
||||||
orm?: ORM,
|
orm?: ORM,
|
||||||
backend?: Backend,
|
backend?: Backend,
|
||||||
|
runtime?: Runtime,
|
||||||
): Promise<DatabaseSetup> {
|
): Promise<DatabaseSetup> {
|
||||||
if (backend === "convex") {
|
if (backend === "convex") {
|
||||||
return "none";
|
return "none";
|
||||||
@@ -32,6 +33,15 @@ export async function getDBSetupChoice(
|
|||||||
label: "Turso",
|
label: "Turso",
|
||||||
hint: "SQLite for Production. Powered by libSQL",
|
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" },
|
{ value: "none" as const, label: "None", hint: "Manual setup" },
|
||||||
];
|
];
|
||||||
} else if (databaseType === "postgres") {
|
} else if (databaseType === "postgres") {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const DatabaseSetupSchema = z
|
|||||||
"prisma-postgres",
|
"prisma-postgres",
|
||||||
"mongodb-atlas",
|
"mongodb-atlas",
|
||||||
"supabase",
|
"supabase",
|
||||||
|
"d1",
|
||||||
"none",
|
"none",
|
||||||
])
|
])
|
||||||
.describe("Database hosting setup");
|
.describe("Database hosting setup");
|
||||||
|
|||||||
@@ -358,6 +358,22 @@ export function processAndValidateFlags(
|
|||||||
process.exit(1);
|
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 (
|
if (
|
||||||
providedFlags.has("runtime") &&
|
providedFlags.has("runtime") &&
|
||||||
options.runtime === "workers" &&
|
options.runtime === "workers" &&
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ import { defineConfig } from "drizzle-kit";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/db/schema",
|
schema: "./src/db/schema",
|
||||||
out: "./src/db/migrations",
|
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",
|
dialect: "turso",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DATABASE_URL || "",
|
url: process.env.DATABASE_URL || "",
|
||||||
@@ -10,4 +20,5 @@ export default defineConfig({
|
|||||||
authToken: process.env.DATABASE_AUTH_TOKEN,
|
authToken: process.env.DATABASE_AUTH_TOKEN,
|
||||||
{{/if}}
|
{{/if}}
|
||||||
},
|
},
|
||||||
|
{{/if}}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export const db = drizzle({ client });
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (eq runtime "workers")}}
|
{{#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 { drizzle } from "drizzle-orm/libsql";
|
||||||
import { env } from "cloudflare:workers";
|
import { env } from "cloudflare:workers";
|
||||||
import { createClient } from "@libsql/client";
|
import { createClient } from "@libsql/client";
|
||||||
@@ -26,3 +32,4 @@ const client = createClient({
|
|||||||
|
|
||||||
export const db = drizzle({ client });
|
export const db = drizzle({ client });
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
|||||||
@@ -5,14 +5,30 @@
|
|||||||
"compatibility_flags": ["nodejs_compat"],
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
"vars": {
|
"vars": {
|
||||||
"NODE_ENV": "production"
|
"NODE_ENV": "production"
|
||||||
// Non-sensitive environment variables (visible in dashboard)
|
// Add public environment variables here
|
||||||
// "CORS_ORIGIN": "https://your-frontend-domain.com",
|
// Example: "CORS_ORIGIN": "https://your-domain.com"
|
||||||
// "BETTER_AUTH_URL": "https://your-worker-domain.workers.dev"
|
|
||||||
}
|
}
|
||||||
// ⚠️ SENSITIVE DATA: Use `wrangler secret put` instead of adding here
|
// For sensitive data, use:
|
||||||
// Don't put these in "vars" - they'll be visible in the dashboard!
|
// wrangler secret put SECRET_NAME
|
||||||
// - DATABASE_URL
|
// Don't add secrets to "vars" - they're visible in the dashboard!
|
||||||
// - DATABASE_AUTH_TOKEN
|
|
||||||
// - GOOGLE_GENERATIVE_AI_API_KEY
|
{{#if (eq dbSetup "d1")}},
|
||||||
// - BETTER_AUTH_SECRET
|
// 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