diff --git a/.changeset/config.json b/.changeset/config.json index 1bd913bad..21e54fbf7 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -8,4 +8,4 @@ "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": ["@better-t-stack/backend", "web"] -} \ No newline at end of file +} diff --git a/.changeset/kind-geese-sell.md b/.changeset/kind-geese-sell.md new file mode 100644 index 000000000..85fbc14d3 --- /dev/null +++ b/.changeset/kind-geese-sell.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +Add SingleStore Helios database support diff --git a/apps/cli/README.md b/apps/cli/README.md index 419039be4..00bcb3f3d 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -36,9 +36,9 @@ Follow the prompts to configure your project or use the `--yes` flag for default | **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex
• Fastify
• None | | **API Layer** | • tRPC (type-safe APIs)
• oRPC (OpenAPI-compatible type-safe APIs)
• None | | **Runtime** | • Bun
• Node.js
• Cloudflare Workers
• None | -| **Database** | • SQLite
• PostgreSQL
• MySQL
• MongoDB
• None | +| **Database** | • SQLite
• PostgreSQL
• MySQL
• MongoDB
• SingleStore
• None | | **ORM** | • Drizzle (TypeScript-first)
• Prisma (feature-rich)
• Mongoose (for MongoDB)
• None | -| **Database Setup** | • Turso (SQLite)
• Cloudflare D1 (SQLite)
• Neon (PostgreSQL)
• Supabase (PostgreSQL)
• Prisma Postgres (via Prisma Accelerate)
• MongoDB Atlas
• None (manual setup) | +| **Database Setup** | • Turso (SQLite)
• Cloudflare D1 (SQLite)
• Neon (PostgreSQL)
• Supabase (PostgreSQL)
• Prisma Postgres (via Prisma Accelerate)
• MongoDB Atlas
• SingleStore Helios (cloud-hosted SingleStore)
• None (manual setup) | | **Authentication** | Better-Auth (email/password, with more options coming soon) | | **Styling** | Tailwind CSS with shadcn/ui components | | **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds) | @@ -53,7 +53,7 @@ Usage: create-better-t-stack [project-directory] [options] Options: -V, --version Output the version number -y, --yes Use default configuration - --database Database type (none, sqlite, postgres, mysql, mongodb) + --database Database type (none, sqlite, postgres, mysql, mongodb, singlestore) --orm ORM type (none, drizzle, prisma, mongoose) --auth Include authentication --no-auth Exclude authentication @@ -65,7 +65,7 @@ Options: --package-manager Package manager (npm, pnpm, bun) --install Install dependencies --no-install Skip installing dependencies - --db-setup Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, docker, none) + --db-setup Database setup (turso, d1, neon, supabase, prisma-postgres, mongodb-atlas, singlestore-helios, docker, none) --web-deploy Web deployment (workers, none) --backend Backend framework (hono, express, elysia, next, convex, fastify, none) --runtime Runtime (bun, node, workers, none) @@ -173,6 +173,11 @@ Create a Cloudflare Workers project: ```bash npx create-better-t-stack my-app --backend hono --runtime workers --database sqlite --orm drizzle --db-setup d1 + +Create a SingleStore project with Helios cloud setup: + +```bash +npx create-better-t-stack my-app --backend hono --runtime node --database singlestore --orm drizzle --db-setup singlestore-helios --api trpc ``` Create a minimal API-only project: @@ -191,6 +196,7 @@ npx create-better-t-stack my-app --frontend none --backend hono --api trpc --dat - **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM. - **Runtime 'none'**: Only available with Convex backend or when backend is 'none'. - **Cloudflare Workers runtime**: Only compatible with Hono backend, Drizzle ORM (or no ORM), and SQLite database (with D1 setup). Not compatible with MongoDB. +- **SingleStore database**: Only compatible with Drizzle ORM. Always uses SingleStore Helios setup (no manual setup option). - **Addons 'none'**: Skips all addons (PWA, Tauri, Starlight, Biome, Husky, Turborepo). - **Examples 'none'**: Skips all example implementations (todo, AI chat). - **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer diff --git a/apps/cli/src/helpers/database-providers/singlestore-helios-setup.ts b/apps/cli/src/helpers/database-providers/singlestore-helios-setup.ts new file mode 100644 index 000000000..b06e08f2d --- /dev/null +++ b/apps/cli/src/helpers/database-providers/singlestore-helios-setup.ts @@ -0,0 +1,83 @@ +import path from "node:path"; +import { log } from "@clack/prompts"; +import fs from "fs-extra"; +import pc from "picocolors"; +import type { ProjectConfig } from "../../types"; +import { + addEnvVariablesToFile, + type EnvVariable, +} from "../project-generation/env-setup"; + +type SingleStoreHeliosConfig = { + connectionString: string; +}; + +async function writeEnvFile( + projectDir: string, + config?: SingleStoreHeliosConfig, +) { + try { + const envPath = path.join(projectDir, "apps/server", ".env"); + const variables: EnvVariable[] = [ + { + key: "DATABASE_URL", + value: + config?.connectionString ?? + "singlestore://username:password@host:port/database?ssl={}", + condition: true, + }, + ]; + await addEnvVariablesToFile(envPath, variables); + } catch (_error) { + log.error("Failed to update environment configuration"); + } +} + +function displayManualSetupInstructions() { + log.info(` +${pc.green("SingleStore Helios Manual Setup Instructions:")} + +1. Sign up for SingleStore Cloud at: + ${pc.blue("https://www.singlestore.com/cloud")} + +2. Create a new workspace from the dashboard + +3. Get your connection string from the workspace details: + Format: ${pc.dim("singlestore://USERNAME:PASSWORD@HOST:PORT/DATABASE?ssl={}")} + +4. Add the connection string to your .env file: + ${pc.dim('DATABASE_URL="your_connection_string"')} + +${pc.yellow("Important:")} +- The connection string MUST include ${pc.bold("ssl={}")} at the end +- Use the singlestore:// protocol for SingleStore connections +- SingleStore requires SSL connections for cloud deployments`); +} + +export async function setupSingleStoreHelios(config: ProjectConfig) { + const { projectDir } = config; + + try { + const serverDir = path.join(projectDir, "apps/server"); + await fs.ensureDir(serverDir); + await writeEnvFile(projectDir); + + log.success( + pc.green( + "SingleStore Helios setup complete! Please update the connection string in .env file.", + ), + ); + + displayManualSetupInstructions(); + } catch (error) { + log.error(pc.red("SingleStore Helios setup failed")); + if (error instanceof Error) { + log.error(pc.red(error.message)); + } + + try { + await writeEnvFile(projectDir); + displayManualSetupInstructions(); + } catch {} + } +} diff --git a/apps/cli/src/helpers/project-generation/env-setup.ts b/apps/cli/src/helpers/project-generation/env-setup.ts index 210c4caff..5c919bd2f 100644 --- a/apps/cli/src/helpers/project-generation/env-setup.ts +++ b/apps/cli/src/helpers/project-generation/env-setup.ts @@ -185,6 +185,7 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { dbSetup === "mongodb-atlas" || dbSetup === "neon" || dbSetup === "supabase" || + dbSetup === "singlestore-helios" || dbSetup === "d1" || dbSetup === "docker"; diff --git a/apps/cli/src/helpers/setup/db-setup.ts b/apps/cli/src/helpers/setup/db-setup.ts index 48d47e17e..a8be87e0c 100644 --- a/apps/cli/src/helpers/setup/db-setup.ts +++ b/apps/cli/src/helpers/setup/db-setup.ts @@ -10,6 +10,7 @@ import { setupDockerCompose } from "../database-providers/docker-compose-setup"; import { setupMongoDBAtlas } from "../database-providers/mongodb-atlas-setup"; import { setupNeonPostgres } from "../database-providers/neon-setup"; import { setupPrismaPostgres } from "../database-providers/prisma-postgres-setup"; +import { setupSingleStoreHelios } from "../database-providers/singlestore-helios-setup"; import { setupSupabase } from "../database-providers/supabase-setup"; import { setupTurso } from "../database-providers/turso-setup"; @@ -68,6 +69,12 @@ export async function setupDatabase(config: ProjectConfig) { devDependencies: ["drizzle-kit"], projectDir: serverDir, }); + } else if (database === "singlestore") { + await addPackageDependency({ + dependencies: ["drizzle-orm", "mysql2"], + devDependencies: ["drizzle-kit"], + projectDir: serverDir, + }); } } else if (orm === "mongoose") { await addPackageDependency({ @@ -93,6 +100,8 @@ export async function setupDatabase(config: ProjectConfig) { } } else if (database === "mongodb" && dbSetup === "mongodb-atlas") { await setupMongoDBAtlas(config); + } else if (database === "singlestore" && dbSetup === "singlestore-helios") { + await setupSingleStoreHelios(config); } } catch (error) { s.stop(pc.red("Failed to set up database")); diff --git a/apps/cli/src/prompts/database-setup.ts b/apps/cli/src/prompts/database-setup.ts index 2d27d7c47..a21f3038a 100644 --- a/apps/cli/src/prompts/database-setup.ts +++ b/apps/cli/src/prompts/database-setup.ts @@ -91,6 +91,8 @@ export async function getDBSetupChoice( }, { value: "none" as const, label: "None", hint: "Manual setup" }, ]; + } else if (databaseType === "singlestore") { + return "singlestore-helios"; } else { return "none"; } diff --git a/apps/cli/src/prompts/database.ts b/apps/cli/src/prompts/database.ts index 16c717d44..6e0f645e4 100644 --- a/apps/cli/src/prompts/database.ts +++ b/apps/cli/src/prompts/database.ts @@ -47,6 +47,11 @@ export async function getDatabaseChoice( label: "MongoDB", hint: "open-source NoSQL database that stores data in JSON-like documents called BSON", }); + databaseOptions.push({ + value: "singlestore", + label: "SingleStore", + hint: "high-performance distributed SQL database for real-time analytics", + }); } const response = await select({ diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index a769345ff..30dab67f7 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -1,7 +1,7 @@ import z from "zod"; export const DatabaseSchema = z - .enum(["none", "sqlite", "postgres", "mysql", "mongodb"]) + .enum(["none", "sqlite", "postgres", "mysql", "mongodb", "singlestore"]) .describe("Database type"); export type Database = z.infer; @@ -70,6 +70,7 @@ export const DatabaseSetupSchema = z "prisma-postgres", "mongodb-atlas", "supabase", + "singlestore-helios", "d1", "docker", "none", diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index c7b4b6f8c..33fa7c645 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -129,6 +129,64 @@ export function validateWorkersCompatibility( "Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.", ); } + + if ( + providedFlags.has("runtime") && + options.runtime === "workers" && + config.database === "singlestore" + ) { + exitWithError( + "SingleStore is not supported on Cloudflare Workers. Use Bun or Node.js runtimes.", + ); + } + + if ( + providedFlags.has("database") && + config.database === "singlestore" && + config.runtime === "workers" + ) { + exitWithError( + "SingleStore is not supported on Cloudflare Workers. Use Bun or Node.js runtimes.", + ); + } +} + +export function validateSingleStoreCompatibility( + providedFlags: Set, + options: CLIInput, + config: Partial, +) { + if ( + providedFlags.has("database") && + options.database === "singlestore" && + config.orm && + config.orm !== "drizzle" + ) { + exitWithError( + `SingleStore database requires Drizzle ORM. Current ORM: ${config.orm}. Please use '--orm drizzle' or choose a different database.`, + ); + } + + if ( + providedFlags.has("orm") && + config.orm && + config.orm !== "drizzle" && + config.database === "singlestore" + ) { + exitWithError( + `ORM '${config.orm}' is not compatible with SingleStore database. SingleStore requires Drizzle ORM. Please use '--orm drizzle' or choose a different database.`, + ); + } + + if ( + providedFlags.has("dbSetup") && + options.dbSetup === "singlestore-helios" && + config.database !== "singlestore" + ) { + exitWithError( + `SingleStore Helios setup (--db-setup singlestore-helios) requires SingleStore database. Current database: ${config.database}. Please use '--database singlestore' or choose a different database setup.`, + ); + } } export function coerceBackendPresets(config: Partial) { diff --git a/apps/cli/src/validation.ts b/apps/cli/src/validation.ts index b04dc292a..613153d5a 100644 --- a/apps/cli/src/validation.ts +++ b/apps/cli/src/validation.ts @@ -20,6 +20,7 @@ import { validateAddonsAgainstFrontends, validateApiFrontendCompatibility, validateExamplesCompatibility, + validateSingleStoreCompatibility, validateWebDeployRequiresWebFrontend, validateWorkersCompatibility, } from "./utils/compatibility-rules"; @@ -247,6 +248,18 @@ export function processAndValidateFlags( ); } + if ( + providedFlags.has("database") && + providedFlags.has("orm") && + config.database === "singlestore" && + config.orm && + config.orm !== "drizzle" + ) { + exitWithError( + "SingleStore database requires Drizzle ORM. Please use '--orm drizzle' or choose a different database.", + ); + } + if ( providedFlags.has("database") && providedFlags.has("orm") && @@ -349,6 +362,28 @@ export function processAndValidateFlags( ); } + if ( + providedFlags.has("dbSetup") && + (config.database ? providedFlags.has("database") : true) && + config.dbSetup === "singlestore-helios" && + config.database !== "singlestore" + ) { + exitWithError( + "SingleStore Helios setup requires SingleStore database. Please use '--database singlestore' or choose a different setup.", + ); + } + + if ( + providedFlags.has("database") && + providedFlags.has("dbSetup") && + config.database === "singlestore" && + config.dbSetup === "none" + ) { + exitWithError( + "SingleStore database requires SingleStore Helios setup. Please use '--db-setup singlestore-helios' or omit the --db-setup flag.", + ); + } + if (config.dbSetup === "d1") { if ( (providedFlags.has("dbSetup") && providedFlags.has("database")) || @@ -396,6 +431,7 @@ export function processAndValidateFlags( } validateWorkersCompatibility(providedFlags, options, config); + validateSingleStoreCompatibility(providedFlags, options, config); const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f), diff --git a/apps/cli/templates/db/drizzle/singlestore/drizzle.config.ts.hbs b/apps/cli/templates/db/drizzle/singlestore/drizzle.config.ts.hbs new file mode 100644 index 000000000..581473b56 --- /dev/null +++ b/apps/cli/templates/db/drizzle/singlestore/drizzle.config.ts.hbs @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema", + out: "./src/db/migrations", + dialect: "singlestore", + dbCredentials: { + url: process.env.DATABASE_URL || "", + }, +}); \ No newline at end of file diff --git a/apps/cli/templates/db/drizzle/singlestore/src/db/index.ts.hbs b/apps/cli/templates/db/drizzle/singlestore/src/db/index.ts.hbs new file mode 100644 index 000000000..a0be3fdb1 --- /dev/null +++ b/apps/cli/templates/db/drizzle/singlestore/src/db/index.ts.hbs @@ -0,0 +1,6 @@ +import mysql from "mysql2/promise"; +import { drizzle } from "drizzle-orm/singlestore"; + +const pool = mysql.createPool(process.env.DATABASE_URL || ""); + +export const db = drizzle({ client: pool }); diff --git a/apps/cli/templates/examples/todo/server/drizzle/singlestore/src/db/schema/todo.ts b/apps/cli/templates/examples/todo/server/drizzle/singlestore/src/db/schema/todo.ts new file mode 100644 index 000000000..025d6c29f --- /dev/null +++ b/apps/cli/templates/examples/todo/server/drizzle/singlestore/src/db/schema/todo.ts @@ -0,0 +1,7 @@ +import { singlestoreTable, varchar, bigint, boolean } from "drizzle-orm/singlestore-core"; + +export const todo = singlestoreTable("todo", { + id: bigint("id", { mode: "number" }).primaryKey().autoincrement(), + text: varchar("text", { length: 255 }).notNull(), + completed: boolean("completed").default(false).notNull(), +}); diff --git a/apps/cli/test/cli.smoke.test.ts b/apps/cli/test/cli.smoke.test.ts index 8fe3c1080..6eb50c60d 100644 --- a/apps/cli/test/cli.smoke.test.ts +++ b/apps/cli/test/cli.smoke.test.ts @@ -1146,6 +1146,313 @@ describe("create-better-t-stack smoke", () => { orm: "mongoose", }); }); + + it("scaffolds with SingleStore + Drizzle", async () => { + const projectName = "app-singlestore-drizzle"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--no-auth", + "--addons", + "none", + "--db-setup", + "singlestore-helios", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertProjectStructure(projectDir, { + hasWeb: true, + hasServer: true, + hasDatabase: true, + }); + assertBtsConfig(projectDir, { + database: "singlestore", + orm: "drizzle", + }); + }); + + it("SingleStore project generates correct drizzle setup and dependencies", async () => { + const projectName = "app-singlestore-setup"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--no-auth", + "--addons", + "none", + "--db-setup", + "singlestore-helios", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + const serverDir = join(projectDir, "apps", "server"); + const drizzleConfigPath = join(serverDir, "drizzle.config.ts"); + expect(existsSync(drizzleConfigPath)).toBe(true); + const drizzleConfig = readFileSync(drizzleConfigPath, "utf8"); + expect(drizzleConfig).toContain('dialect: "singlestore"'); + const dbIndexPath = join(serverDir, "src", "db", "index.ts"); + expect(existsSync(dbIndexPath)).toBe(true); + const dbIndex = readFileSync(dbIndexPath, "utf8"); + expect(dbIndex).toContain("drizzle-orm/singlestore"); + expect(dbIndex).toContain("mysql2/promise"); + const packageJsonPath = join(serverDir, "package.json"); + expect(existsSync(packageJsonPath)).toBe(true); + const packageJson = readJsonSync(packageJsonPath); + expect(packageJson.dependencies).toHaveProperty("mysql2"); + }); + + it("SingleStore project generates proper schema structure", async () => { + const projectName = "app-singlestore-schema"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--no-auth", + "--addons", + "none", + "--db-setup", + "singlestore-helios", + "--examples", + "todo", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + const serverDir = join(projectDir, "apps", "server"); + const schemaDir = join(serverDir, "src", "db", "schema"); + expect(existsSync(schemaDir)).toBe(true); + const schemaFiles = require("node:fs").readdirSync(schemaDir); + const schemaFile = schemaFiles.find((file: string) => + file.endsWith(".ts"), + ); + + if (schemaFile) { + const schemaPath = join(schemaDir, schemaFile); + const schemaContent = readFileSync(schemaPath, "utf8"); + expect(schemaContent).toContain("singlestoreTable"); + expect(schemaContent).toContain('from "drizzle-orm/singlestore-core"'); + expect(schemaContent).not.toContain('from "drizzle-orm/singlestore"'); + } + }); + + it("SingleStore project generates .env with correct SSL configuration", async () => { + const projectName = "app-singlestore-env"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--no-auth", + "--addons", + "none", + "--db-setup", + "singlestore-helios", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + const serverDir = join(projectDir, "apps", "server"); + const envPath = join(serverDir, ".env"); + expect(existsSync(envPath)).toBe(true); + const envContent = readFileSync(envPath, "utf8"); + expect(envContent).toContain("DATABASE_URL"); + const lines = envContent.split("\n"); + const databaseUrlLine = lines.find((line) => + line.startsWith("DATABASE_URL="), + ); + expect(databaseUrlLine).toBeTruthy(); + + const databaseUrl = databaseUrlLine + ?.split("=", 2)[1] + ?.replace(/['"]/g, ""); + expect(databaseUrl).toBeTruthy(); + expect(databaseUrl).toMatch(/singlestore:\/\/.*\?ssl/); + expect(envContent).toContain("?ssl="); + }); + + describe("SingleStore compatibility matrix", () => { + it("scaffolds SingleStore with authentication enabled", async () => { + const projectName = "singlestore-auth"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--auth", + "--db-setup", + "singlestore-helios", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertProjectStructure(projectDir, { + hasWeb: true, + hasServer: true, + hasDatabase: true, + hasAuth: true, + }); + assertBtsConfig(projectDir, { + database: "singlestore", + orm: "drizzle", + auth: true, + }); + }); + + it("scaffolds SingleStore with TODO example", async () => { + const projectName = "singlestore-todo"; + await runCli( + [ + projectName, + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "trpc", + "--no-auth", + "--db-setup", + "singlestore-helios", + "--examples", + "todo", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + + const projectDir = join(workdir, projectName); + assertScaffoldedProject(projectDir); + assertProjectStructure(projectDir, { + hasWeb: true, + hasServer: true, + hasDatabase: true, + }); + assertBtsConfig(projectDir, { + database: "singlestore", + orm: "drizzle", + examples: ["todo"], + }); + + const todoSchemaPath = join( + projectDir, + "apps", + "server", + "src", + "db", + "schema", + "todo.ts", + ); + expect(existsSync(todoSchemaPath)).toBe(true); + const todoSchemaContent = readFileSync(todoSchemaPath, "utf8"); + expect(todoSchemaContent).toContain("singlestoreTable"); + expect(todoSchemaContent).toContain("bigint("); + }); + }); }); describe("addon combinations", () => { @@ -1391,6 +1698,138 @@ describe("create-better-t-stack smoke", () => { ); }); + it("rejects SingleStore with Prisma ORM", async () => { + await runCliExpectingError( + [ + "invalid-singlestore-prisma", + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "prisma", + "--api", + "none", + "--no-auth", + "--addons", + "none", + "--db-setup", + "none", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + }); + + it("rejects SingleStore with Mongoose ORM", async () => { + await runCliExpectingError( + [ + "invalid-singlestore-mongoose", + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "singlestore", + "--orm", + "mongoose", + "--api", + "none", + "--no-auth", + "--addons", + "none", + "--db-setup", + "none", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + }); + + it("rejects singlestore-helios setup with non-SingleStore database", async () => { + await runCliExpectingError( + [ + "invalid-helios-postgres", + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "bun", + "--database", + "postgres", + "--orm", + "prisma", + "--api", + "none", + "--no-auth", + "--addons", + "none", + "--db-setup", + "singlestore-helios", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + }); + + it("rejects SingleStore database with none db-setup", async () => { + await runCliExpectingError( + [ + "invalid-singlestore-none", + "--yes", + "--frontend", + "tanstack-router", + "--backend", + "hono", + "--runtime", + "node", + "--database", + "singlestore", + "--orm", + "drizzle", + "--api", + "none", + "--no-auth", + "--addons", + "none", + "--db-setup", + "none", + "--examples", + "none", + "--package-manager", + "bun", + "--no-install", + "--no-git", + ], + workdir, + ); + }); + it("rejects incompatible frontend and API combinations", async () => { await runCliExpectingError( [ @@ -2437,6 +2876,7 @@ describe("create-better-t-stack smoke", () => { "app-sqlite-drizzle", "app-postgres-prisma", "app-mongo-mongoose", + "app-singlestore-drizzle", "app-biome", "app-multi-addons", "app-trpc", @@ -2459,6 +2899,7 @@ describe("create-better-t-stack smoke", () => { "app-orpc-solid", "app-backend-next", "app-node-runtime", + "singlestore-hono-node", ].forEach((n) => projectNames.add(n)); const detectPackageManager = ( diff --git a/apps/cli/test/programmatic-api.test.ts b/apps/cli/test/programmatic-api.test.ts index 3fe7c4540..e93a35689 100644 --- a/apps/cli/test/programmatic-api.test.ts +++ b/apps/cli/test/programmatic-api.test.ts @@ -172,6 +172,23 @@ describe("Programmatic API - Fast Tests", () => { }); }, 15000); + test("creates project with SingleStore + Drizzle", async () => { + const result = await init("singlestore-app", { + yes: true, + database: "singlestore", + orm: "drizzle", + dbSetup: "singlestore-helios", + install: false, + git: false, + }); + + expect(result.success).toBe(true); + assertBtsConfig(result.projectDirectory, { + database: "singlestore", + orm: "drizzle", + }); + }, 15000); + test("creates project with oRPC API", async () => { const result = await init("orpc-app", { yes: true, @@ -261,6 +278,19 @@ describe("Programmatic API - Fast Tests", () => { ).rejects.toThrow(/requires Mongoose or Prisma/); }); + test("handles incompatible SingleStore + Prisma combination", async () => { + await expect( + init("singlestore-prisma", { + yes: true, + database: "singlestore", + orm: "prisma", + install: false, + git: false, + yolo: false, + }), + ).rejects.toThrow(/SingleStore database requires Drizzle/); + }); + test("handles auth without database", async () => { await expect( init("auth-no-db", { diff --git a/apps/web/content/docs/cli/compatibility.mdx b/apps/web/content/docs/cli/compatibility.mdx index b05811f0f..a5628b039 100644 --- a/apps/web/content/docs/cli/compatibility.mdx +++ b/apps/web/content/docs/cli/compatibility.mdx @@ -17,11 +17,13 @@ The CLI validates option combinations to ensure generated projects work correctl | `postgres` | `drizzle`, `prisma` | Advanced relational database | | `mysql` | `drizzle`, `prisma` | Traditional relational database | | `mongodb` | `mongoose`, `prisma` | Document database, requires specific ORMs | +| `singlestore` | `drizzle` | High-performance distributed SQL database | | `none` | `none` | No database setup | ### Restrictions - **MongoDB + Drizzle**: ❌ Not supported - Drizzle doesn't support MongoDB +- **SingleStore + Prisma/Mongoose**: ❌ Not supported - SingleStore only supports Drizzle ORM - **Database without ORM**: ❌ Not supported - Database requires an ORM for code generation - **ORM without Database**: ❌ Not supported - ORM requires a database target @@ -104,7 +106,7 @@ create-better-t-stack --frontend nuxt --api orpc ### Frontend Restrictions - **Multiple Web Frontends**: ❌ Only one web framework allowed -- **Multiple Native Frontends**: ❌ Only one native framework allowed +- **Multiple Native Frontends**: ❌ Only one native framework allowed - **Web + Native**: ✅ One web and one native framework allowed ```bash @@ -127,6 +129,7 @@ create-better-t-stack --frontend next native-nativewind | `supabase` | `postgres` | PostgreSQL with additional features | | `prisma-postgres` | `postgres` | Managed PostgreSQL via Prisma | | `mongodb-atlas` | `mongodb` | Managed MongoDB | +| `singlestore-helios` | `singlestore` | Cloud-hosted SingleStore database | | `docker` | `postgres`, `mysql`, `mongodb` | Not compatible with `sqlite` or Workers | ### Special Cases @@ -177,7 +180,7 @@ create-better-t-stack --auth --database postgres --orm drizzle --backend hono - Requires a database when backend is present (except Convex) - Cannot be used with `--backend none` and `--database none` -### AI Example +### AI Example - Not compatible with `--backend elysia` - Not compatible with `--frontend solid` diff --git a/apps/web/content/docs/cli/index.mdx b/apps/web/content/docs/cli/index.mdx index b423509cd..7dec6ba49 100644 --- a/apps/web/content/docs/cli/index.mdx +++ b/apps/web/content/docs/cli/index.mdx @@ -30,10 +30,10 @@ create-better-t-stack [project-directory] [options] - `--frontend `: Web and/or native frameworks (see [Options](/docs/cli/options#frontend)) - `--backend `: `hono`, `express`, `fastify`, `elysia`, `next`, `convex`, `none` - `--runtime `: `bun`, `node`, `workers` (`none` only with `--backend convex` or `--backend none`) -- `--database `: `none`, `sqlite`, `postgres`, `mysql`, `mongodb` +- `--database `: `none`, `sqlite`, `postgres`, `mysql`, `mongodb`, `singlestore` - `--orm `: `none`, `drizzle`, `prisma`, `mongoose` - `--api `: `none`, `trpc`, `orpc` -- `--db-setup `: `none`, `turso`, `d1`, `neon`, `supabase`, `prisma-postgres`, `mongodb-atlas`, `docker` +- `--db-setup `: `none`, `turso`, `d1`, `neon`, `supabase`, `prisma-postgres`, `mongodb-atlas`, `singlestore-helios`, `docker` - `--examples `: `none`, `todo`, `ai` - `--web-deploy `: `none`, `workers` - `--directory-conflict `: `merge`, `overwrite`, `increment`, `error` diff --git a/apps/web/content/docs/cli/options.mdx b/apps/web/content/docs/cli/options.mdx index 7650925bf..96bcdf17a 100644 --- a/apps/web/content/docs/cli/options.mdx +++ b/apps/web/content/docs/cli/options.mdx @@ -87,9 +87,10 @@ Database type to use: - `none`: No database - `sqlite`: SQLite database -- `postgres`: PostgreSQL database +- `postgres`: PostgreSQL database - `mysql`: MySQL database - `mongodb`: MongoDB database +- `singlestore`: SingleStore database ```bash create-better-t-stack --database postgres @@ -119,6 +120,7 @@ Database hosting/setup provider: - `supabase`: Supabase (PostgreSQL) - `prisma-postgres`: Prisma Postgres via Prisma Accelerate - `mongodb-atlas`: MongoDB Atlas +- `singlestore-helios`: SingleStore Helios (cloud-hosted SingleStore) - `docker`: Local Docker containers ```bash diff --git a/apps/web/content/docs/cli/programmatic-api.mdx b/apps/web/content/docs/cli/programmatic-api.mdx index 3538e6559..d3f7397a3 100644 --- a/apps/web/content/docs/cli/programmatic-api.mdx +++ b/apps/web/content/docs/cli/programmatic-api.mdx @@ -132,7 +132,7 @@ interface CreateInput { yes?: boolean; // Skip prompts, use defaults yolo?: boolean; // Bypass validations (not recommended) verbose?: boolean; // Show JSON result (CLI only, programmatic always returns result) - database?: Database; // "none" | "sqlite" | "postgres" | "mysql" | "mongodb" + database?: Database; // "none" | "sqlite" | "postgres" | "mysql" | "mongodb" | "singlestore" orm?: ORM; // "none" | "drizzle" | "prisma" | "mongoose" auth?: boolean; // Include authentication frontend?: Frontend[]; // Array of frontend frameworks diff --git a/apps/web/content/docs/compatibility.mdx b/apps/web/content/docs/compatibility.mdx index 60b5f9d17..8f6ccb780 100644 --- a/apps/web/content/docs/compatibility.mdx +++ b/apps/web/content/docs/compatibility.mdx @@ -11,6 +11,7 @@ description: Valid and invalid combinations across frontend, backend, runtime, d - **API `none`**: No tRPC/oRPC setup; use framework-native APIs - **Database `none`**: Disables ORM and authentication - **ORM `none`**: No ORM setup; manage DB manually +- **SingleStore database**: Only compatible with Drizzle ORM; requires SingleStore Helios setup - **Runtime `none`**: Only with Convex backend or when backend is `none` ## Cloudflare Workers @@ -18,7 +19,7 @@ description: Valid and invalid combinations across frontend, backend, runtime, d - Backend: `hono` only - Database: `sqlite` with Cloudflare D1 - ORM: `drizzle` (or none) -- Not compatible with MongoDB +- Not compatible with MongoDB, SingleStore ## Framework Notes diff --git a/apps/web/content/docs/index.mdx b/apps/web/content/docs/index.mdx index 3e9e76f7c..b89503c08 100644 --- a/apps/web/content/docs/index.mdx +++ b/apps/web/content/docs/index.mdx @@ -251,7 +251,7 @@ See the full list in the [CLI Reference](/docs/cli). Key flags: - `--frontend`: tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none - `--backend`: hono, express, fastify, elysia, next, convex, none - `--runtime`: bun, node, workers, none -- `--database`: sqlite, postgres, mysql, mongodb, none +- `--database`: sqlite, postgres, mysql, mongodb, singlestore, none - `--orm`: drizzle, prisma, mongoose, none - `--api`: trpc, orpc, none - `--addons`: turborepo, pwa, tauri, biome, husky, starlight, none diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index cc24712e7..abf0e7081 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -382,6 +382,57 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { message: "ORM set to 'Prisma' (MongoDB requires Prisma or Mongoose)", }); } + } else if (nextStack.database === "singlestore") { + if (nextStack.orm !== "drizzle") { + notes.database.notes.push( + "SingleStore requires Drizzle ORM. Drizzle will be selected.", + ); + notes.orm.notes.push( + "SingleStore requires Drizzle ORM. It will be selected.", + ); + notes.database.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + changes.push({ + category: "database", + message: "ORM set to 'Drizzle' (SingleStore requires Drizzle)", + }); + } + if (nextStack.dbSetup !== "singlestore-helios") { + notes.database.notes.push( + "SingleStore database selected: DB Setup will be set to 'SingleStore Helios'.", + ); + notes.dbSetup.notes.push( + "SingleStore works best with cloud setup. SingleStore Helios will be selected.", + ); + notes.database.hasIssue = true; + notes.dbSetup.hasIssue = true; + nextStack.dbSetup = "singlestore-helios"; + changed = true; + changes.push({ + category: "database", + message: + "DB Setup set to 'SingleStore Helios' (recommended for SingleStore)", + }); + } + if (nextStack.runtime === "workers") { + notes.runtime.notes.push( + "Cloudflare Workers runtime is not compatible with SingleStore. SQLite will be selected.", + ); + notes.database.notes.push( + "SingleStore is not compatible with Cloudflare Workers runtime. SQLite will be selected.", + ); + notes.runtime.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "sqlite"; + changed = true; + changes.push({ + category: "runtime", + message: + "Database set to 'SQLite' (SingleStore not compatible with Workers)", + }); + } } else { if (nextStack.orm === "mongoose") { notes.database.notes.push( @@ -516,6 +567,41 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { "Database set to 'PostgreSQL' (required by Supabase setup)", }); } + } else if (nextStack.dbSetup === "singlestore-helios") { + if (nextStack.database !== "singlestore") { + notes.dbSetup.notes.push( + "Requires SingleStore. It will be selected.", + ); + notes.database.notes.push( + "SingleStore Helios setup requires SingleStore. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "singlestore"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "Database set to 'SingleStore' (required by SingleStore Helios setup)", + }); + } + if (nextStack.orm !== "drizzle") { + notes.dbSetup.notes.push( + "SingleStore Helios requires Drizzle ORM. It will be selected.", + ); + notes.orm.notes.push( + "SingleStore Helios setup requires Drizzle ORM. It will be selected.", + ); + notes.dbSetup.hasIssue = true; + notes.orm.hasIssue = true; + nextStack.orm = "drizzle"; + changed = true; + changes.push({ + category: "dbSetup", + message: + "ORM set to 'Drizzle' (SingleStore Helios requires Drizzle)", + }); + } } else if (nextStack.dbSetup === "d1") { if (nextStack.database !== "sqlite") { notes.dbSetup.notes.push( @@ -669,6 +755,24 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { }); } + if (nextStack.database === "singlestore") { + notes.runtime.notes.push( + "Cloudflare Workers runtime is not compatible with SingleStore. SQLite will be selected.", + ); + notes.database.notes.push( + "SingleStore is not compatible with Cloudflare Workers runtime. SQLite will be selected.", + ); + notes.runtime.hasIssue = true; + notes.database.hasIssue = true; + nextStack.database = "sqlite"; + changed = true; + changes.push({ + category: "runtime", + message: + "Database set to 'SQLite' (SingleStore not compatible with Workers)", + }); + } + if (nextStack.dbSetup === "docker") { notes.runtime.notes.push( "Cloudflare Workers runtime does not support Docker setup. D1 will be selected.", @@ -945,6 +1049,7 @@ const generateCommand = (stackState: StackState): string => { "supabase", "prisma-postgres", "mongodb-atlas", + "singlestore-helios", "docker", ].includes(stackState.dbSetup); @@ -1635,7 +1740,51 @@ const StackBuilder = () => { TECH_OPTIONS[categoryKey as keyof typeof TECH_OPTIONS] || []; const categoryDisplayName = getCategoryDisplayName(categoryKey); - const filteredOptions = categoryOptions.filter(() => { + const filteredOptions = categoryOptions.filter((option) => { + if (categoryKey === "orm") { + if (stack.database === "mongodb") { + return ( + option.id === "prisma" || + option.id === "mongoose" || + option.id === "none" + ); + } + if (stack.database === "singlestore") { + return option.id === "drizzle"; + } + } + + if (categoryKey === "dbSetup") { + if (stack.database === "singlestore") { + return option.id === "singlestore-helios"; + } + if (stack.database === "sqlite") { + return ["turso", "d1", "docker", "none"].includes( + option.id, + ); + } + if (stack.database === "postgres") { + return [ + "neon", + "supabase", + "prisma-postgres", + "docker", + "none", + ].includes(option.id); + } + if (stack.database === "mysql") { + return ["docker", "none"].includes(option.id); + } + if (stack.database === "mongodb") { + return ["mongodb-atlas", "docker", "none"].includes( + option.id, + ); + } + if (stack.database === "none") { + return option.id === "none"; + } + } + return true; }); diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 792de3448..0a35e33ef 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -246,6 +246,13 @@ export const TECH_OPTIONS: Record< icon: `${ICON_BASE_URL}/mongodb.svg`, color: "from-green-400 to-green-600", }, + { + id: "singlestore", + name: "SingleStore", + description: "High-performance distributed SQL database", + icon: `${ICON_BASE_URL}/singlestore.svg`, + color: "from-purple-500 to-purple-700", + }, { id: "none", name: "No Database", @@ -328,6 +335,13 @@ export const TECH_OPTIONS: Record< icon: `${ICON_BASE_URL}/supabase.svg`, color: "from-emerald-400 to-emerald-600", }, + { + id: "singlestore-helios", + name: "SingleStore Helios", + description: "Cloud-hosted SingleStore database on Helios", + icon: `${ICON_BASE_URL}/singlestore.svg`, + color: "from-purple-500 to-purple-700", + }, { id: "docker", name: "Docker", diff --git a/bun.lock b/bun.lock index 728366ad2..150fc0c5c 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "apps/cli": { "name": "create-better-t-stack", - "version": "2.33.6", + "version": "2.33.8", "bin": { "create-better-t-stack": "dist/cli.js", },