UNPKG

create-quickbase

Version:

A CLI tool to create a Hono backend with Prisma and PostgreSQL

651 lines (548 loc) 15.4 kB
#!/usr/bin/env node const readline = require("readline"); const yargs = require("yargs"); const { execSync } = require("child_process"); const fs = require("fs-extra"); const path = require("path"); const chalk = require("chalk"); const askQuestion = (rl, question) => new Promise((resolve) => rl.question(question, resolve)); const getProjectName = async () => { const projectName = yargs.argv.name; if (projectName) return projectName; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); const name = await askQuestion( rl, "Enter project name (default: awesome-backend): " ); rl.close(); return name || "awesome-backend"; }; const packageManagers = ["npm", "yarn", "pnpm", "bun"]; const getPackageManager = () => { return new Promise((resolve) => { let selectedIndex = 0; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); process.stdin.setRawMode(true); process.stdin.resume(); console.log( chalk.cyan.bold( "\nSelect package manager (use ↑↓ arrows, press enter to confirm):" ) ); const render = () => { console.clear(); console.log( chalk.cyan.bold( "\nSelect package manager (use ↑↓ arrows, press enter to confirm):" ) ); packageManagers.forEach((pm, i) => { if (i === selectedIndex) { console.log(chalk.green.bold(`> ${pm}`)); } else { console.log(chalk.white(` ${pm}`)); } }); }; render(); process.stdin.on("data", (key) => { const byte = key[0]; if (byte === 3) { process.exit(); } if (byte === 13) { process.stdin.setRawMode(false); process.stdin.pause(); rl.close(); resolve(packageManagers[selectedIndex]); return; } if (byte === 27 && key[1] === 91) { // Arrow keys if (key[2] === 65) { // Up selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : packageManagers.length - 1; } if (key[2] === 66) { // Down selectedIndex = selectedIndex < packageManagers.length - 1 ? selectedIndex + 1 : 0; } render(); } }); }); }; const databaseOptions = ["postgresql", "sqlite"]; const getDatabase = () => { return new Promise((resolve) => { let selectedIndex = 0; const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); process.stdin.setRawMode(true); process.stdin.resume(); console.log( chalk.cyan.bold( "\nSelect database (use ↑↓ arrows, press enter to confirm):" ) ); const render = () => { console.clear(); console.log( chalk.cyan.bold( "\nSelect database (use ↑↓ arrows, press enter to confirm):" ) ); databaseOptions.forEach((db, i) => { if (i === selectedIndex) { console.log(chalk.green.bold(`> ${db}`)); } else { console.log(chalk.white(` ${db}`)); } }); }; render(); process.stdin.on("data", (key) => { const byte = key[0]; if (byte === 3) { process.exit(); } if (byte === 13) { process.stdin.setRawMode(false); process.stdin.pause(); rl.close(); resolve(databaseOptions[selectedIndex]); return; } if (byte === 27 && key[1] === 91) { // Arrow keys if (key[2] === 65) { // Up selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : databaseOptions.length - 1; } if (key[2] === 66) { // Down selectedIndex = selectedIndex < databaseOptions.length - 1 ? selectedIndex + 1 : 0; } render(); } }); }); }; const execCommand = (command, options = {}) => { try { return execSync(command, { ...options, encoding: "utf8" }); } catch (error) { console.error(`Error executing command: ${command}`); process.exit(1); } }; const initScripts = { npm: "npm init -y", yarn: "yarn init -y", pnpm: "pnpm init", bun: "bun init -y", }; const createProjectStructure = async (projectName) => { const projectPath = path.join(process.cwd(), projectName); const packageManager = await getPackageManager(); const database = await getDatabase(); // Create project directory fs.mkdirSync(projectPath); process.chdir(projectPath); // Initialize package.json execCommand(initScripts[packageManager]); // Update package.json first const packageJson = { name: projectName, version: "1.0.0", main: "index.js", type: "module", scripts: { dev: "tsx watch src/index.ts", start: "npx tsx src/index.ts", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", }, keywords: [], author: "", license: "ISC", description: "", }; fs.writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); // Install dependencies const dependencies = [ "@hono/node-server", "@prisma/client", "bcrypt", "dotenv", "hono", "zod", ]; const devDependencies = [ "@types/bcrypt", "@types/node", "prisma", "tsx", "typescript", ]; console.log(chalk.blue("Installing dependencies...")); execCommand(`${packageManager} add ${dependencies.join(" ")}`); console.log(chalk.blue("Installing dev dependencies...")); const devFlag = packageManager === "npm" ? "--save-dev" : "-D"; execCommand(`${packageManager} add ${devFlag} ${devDependencies.join(" ")}`); // Initialize TypeScript console.log(chalk.blue("Setting up TypeScript...")); fs.writeFileSync( "tsconfig.json", JSON.stringify( { compilerOptions: { target: "ES2020", module: "ESNext", moduleResolution: "node", esModuleInterop: true, strict: true, outDir: "dist", rootDir: "src", }, include: ["src/**/*"], }, null, 2 ) ); // Create project structure fs.mkdirSync("src"); fs.mkdirSync("src/controllers"); fs.mkdirSync("src/middleware"); fs.mkdirSync("src/routes"); fs.mkdirSync("src/tools"); fs.mkdirSync("prisma"); // Create basic files fs.writeFileSync( ".env", database === "postgresql" ? `DATABASE_URL="postgresql://mydb:@localhost:5432/mydb?schema=public" JWT_SECRET="SomeRandomString" ` : `DATABASE_URL="file:./dev.db" JWT_SECRET="SomeRandomString"` ); fs.writeFileSync( ".gitignore", `node_modules .env dist .DS_Store` ); // Create Prisma schema fs.writeFileSync( "prisma/schema.prisma", `generator client { provider = "prisma-client-js" } datasource db { provider = "${database}" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique name String password String posts Post[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model Post { id String @id @default(uuid()) title String content String published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } ` ); // Create main application file fs.writeFileSync( "src/index.ts", `import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; import { cors } from "hono/cors"; import "dotenv/config"; import { PrismaClient } from "@prisma/client"; import v1 from "./routes/v1.js"; export const prisma = new PrismaClient(); const app = new Hono(); app.use("*", cors()); app.use("*", logger()); async function main() { app.route("/v1", v1); app.get("/", (c) => c.text("Hello World")); } main(); const port = process.env.PORT || 4000; console.log(\`Server is running: http://localhost:\${port}\`); serve({ fetch: app.fetch, port: Number(port), }); ` ); // Create routes file fs.writeFileSync( "src/routes/v1.ts", `import { Hono } from "hono"; import { UserController } from "../controllers/user.controller"; import { userAuth } from "../middleware/userAuth"; import { PostController } from "../controllers/post.controller"; const v1 = new Hono(); // User routes v1.post("/users/register", UserController.register); v1.post("/users/login", UserController.login); // Post routes v1.post("/posts", userAuth, PostController.create); v1.get("/posts", userAuth, PostController.index); v1.get("/posts/:id", userAuth, PostController.show); export default v1; ` ); // Create controllers fs.writeFileSync( "src/controllers/user.controller.ts", `import { Context } from "hono"; import { z } from "zod"; import { comparePassword, hashPassword, resData, resError, } from "../tools/utils"; import { sign } from "hono/jwt"; import { prisma } from ".."; const registerSchema = z.object({ email: z.string().email(), name: z.string().min(2), password: z.string().min(6), }); const loginSchema = z.object({ email: z.string().email(), password: z.string().min(1), }); export class UserController { static async register(c: Context) { try { const body = await c.req.json(); const result = registerSchema.safeParse(body); if (!result.success) { return resError(c, { error: result.error }); } const userFound = await prisma.user.findUnique({ where: { email: result.data.email }, }); if (userFound) { return resError(c, { error: "User already exists" }); } const hashedPassword = await hashPassword(result.data.password); const user = await prisma.user.create({ data: { ...result.data, password: hashedPassword }, omit: { password: true }, }); const token = await sign( { id: user.id }, process.env.JWT_SECRET as string ); return resData(c, { user, token }); } catch (error) { console.log(error); return resError(c, { error: "Internal server error" }, 500); } } static async login(c: Context) { try { const body = await c.req.json(); const result = loginSchema.safeParse(body); if (!result.success) { return resError(c, { error: result.error }); } const user = await prisma.user.findUnique({ where: { email: result.data.email }, }); if (!user) { return resError(c, { error: "User not found" }); } const isPasswordValid = await comparePassword( result.data.password, user.password ); if (!isPasswordValid) { return resError(c, { error: "Invalid credentials" }); } const token = await sign( { id: user.id }, process.env.JWT_SECRET as string ); return resData(c, { user: { ...user, password: "" }, token }); } catch (error) { console.log(error); return resError(c, { error: "Internal server error" }, 500); } } } ` ); fs.writeFileSync( "src/controllers/post.controller.ts", `import { Context } from "hono"; import { z } from "zod"; import { resData, resError } from "../tools/utils"; import { prisma } from ".."; const createPostSchema = z.object({ title: z.string().min(1), content: z.string().min(1), }); export class PostController { static async create(c: Context) { try { const body = await c.req.json(); const user = c.get("user"); const result = createPostSchema.safeParse(body); if (!result.success) { return resError(c, { error: result.error }); } const post = await prisma.post.create({ data: { ...result.data, author: { connect: { id: user.id } } }, }); return resData(c, { post }); } catch (error) { console.log(error); return resError(c, { error: "Internal server error" }, 500); } } static async index(c: Context) { const user = c.get("user"); const page = Number(c.req.query("p")) || 1; const size = Number(c.req.query("s")) || 10; const skip = (page - 1) * size; try { const posts = await prisma.post.findMany({ where: { authorId: user.id }, take: size, skip, }); return resData(c, posts); } catch (error) { console.log(error); return resError(c, { error: "Internal server error" }, 500); } } static async show(c: Context) { try { const id = c.req.param("id"); const post = await prisma.post.findUnique({ where: { id }, include: { author: { omit: { password: true }, }, }, }); if (!post) { return resError(c, { error: "Post not found" }); } return resData(c, post); } catch (error) { console.log(error); return resError(c, { error: "Internal server error" }, 500); } } } ` ); // Create utils file fs.writeFileSync( "src/tools/utils.ts", `import * as bcrypt from "bcrypt"; import { Context } from "hono"; import { StatusCode } from "hono/utils/http-status"; const hashPassword = async (password: string): Promise<string> => { const saltRounds = 10; return bcrypt.hash(password, saltRounds); }; const comparePassword = async ( password: string, hashedPassword: string ): Promise<boolean> => { return bcrypt.compare(password, hashedPassword); }; const resData = (c: Context, data: any, status: StatusCode = 200) => { c.status(status); return c.json(data); }; const resError = (c: Context, data: any, status: StatusCode = 400) => { c.status(status); return c.json(data); }; export { hashPassword, comparePassword, resData, resError }; ` ); // Creating middleware fs.writeFileSync( "src/middleware/userAuth.ts", `import { verify } from "hono/jwt"; import type { Next } from "hono"; import type { Context } from "hono"; import { prisma } from ".."; export const userAuth = async (c: Context, next: Next) => { try { const token = c.req.header("token"); if (!token) { return c.json({ error: "Unauthorized" }, 401); } const payload = await verify(token, process.env.JWT_SECRET as string); const user = await prisma.user.findUnique({ where: { id: payload.id as string }, }); if (!user) { return c.json({ error: "User not found" }, 401); } c.set("user", user); await next(); } catch (error) { return c.json({ error: "Invalid token" }, 401); } }; ` ); console.log(chalk.green(`\nProject ${projectName} created successfully!`)); console.log(chalk.blue("\nNext steps:")); console.log("1. cd", projectName); console.log(`2. ${packageManager} run prisma:generate`); console.log(`3. ${packageManager} run prisma:migrate`); console.log(`4. ${packageManager} run dev`); }; const main = async () => { const projectName = await getProjectName(); await createProjectStructure(projectName); }; main().catch((error) => { console.error(chalk.red("Error:"), error); process.exit(1); });