diff --git a/AGENTS.md b/AGENTS.md index 3d6e459..ba5b073 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ This repository contains planning artifacts and the early implementation scaffol - Runtime and tooling emphasis: **Bun**, Turborepo, Drizzle, BetterAuth, Hono. ## Assistant Identity / Model Context -- If asked which model is running, respond with: **GPT-5.2-Codex, created by OpenAI**. +- If asked which model is running, reply with the runtime-configured model identifier when available; otherwise provide a neutral capability-focused response. ## Current Strategic Inputs Primary planning docs to align with before implementation: diff --git a/betterbase/.gitignore b/betterbase/.gitignore index 8583250..51c8bb1 100644 --- a/betterbase/.gitignore +++ b/betterbase/.gitignore @@ -3,7 +3,23 @@ node_modules .turbo dist .next + +.vscode/ +.idea/ + .env .env.* +.env.local +.env.test !.env.example + +*.log +npm-debug.log +yarn-error.log +pnpm-debug.log + +coverage/ +.cache/ +.parcel-cache/ + .DS_Store diff --git a/betterbase/apps/cli/package.json b/betterbase/apps/cli/package.json index 3432b23..191344d 100644 --- a/betterbase/apps/cli/package.json +++ b/betterbase/apps/cli/package.json @@ -10,5 +10,11 @@ "build": "bun build ./src/index.ts --outfile ./dist/index.js --target bun", "dev": "bun run src/index.ts", "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@betterbase/cli": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.3" } } diff --git a/betterbase/apps/cli/src/index.ts b/betterbase/apps/cli/src/index.ts index d74eb82..5a4ec6e 100644 --- a/betterbase/apps/cli/src/index.ts +++ b/betterbase/apps/cli/src/index.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun /** * Legacy bb wrapper entrypoint. @@ -6,10 +6,16 @@ * Forwards execution to the canonical CLI implementation in packages/cli. */ export async function runLegacyCli(): Promise { - const cliModule = await import('../../../packages/cli/src/index'); - await cliModule.runCli(process.argv); + const { runCli } = await import('@betterbase/cli'); + await runCli(process.argv); } if (import.meta.main) { - await runLegacyCli(); + (async () => { + await runLegacyCli(); + })().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(message); + process.exitCode = 1; + }); } diff --git a/betterbase/apps/cli/tsconfig.json b/betterbase/apps/cli/tsconfig.json index a47cede..4031161 100644 --- a/betterbase/apps/cli/tsconfig.json +++ b/betterbase/apps/cli/tsconfig.json @@ -2,9 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "dist", - "rootDir": "src" + "rootDir": ".", + "types": ["bun"] }, - "include": [ - "src" - ] + "include": ["src", "test"] } diff --git a/betterbase/packages/cli/package.json b/betterbase/packages/cli/package.json index 5ed2153..5b8543d 100644 --- a/betterbase/packages/cli/package.json +++ b/betterbase/packages/cli/package.json @@ -20,6 +20,10 @@ }, "devDependencies": { "@types/bun": "^1.3.9", - "typescript": "^5.6.0" - } + "typescript": "^5.9.3" + }, + "exports": { + ".": "./src/index.ts" + }, + "main": "./src/index.ts" } diff --git a/betterbase/packages/cli/src/build.ts b/betterbase/packages/cli/src/build.ts index 4e34db1..198205e 100644 --- a/betterbase/packages/cli/src/build.ts +++ b/betterbase/packages/cli/src/build.ts @@ -13,7 +13,8 @@ export async function buildStandaloneCli(): Promise { }); if (!result.success) { - throw new Error(`Build failed with ${result.logs.length} error(s).`); + const diagnostics = result.logs.map((log) => (typeof log === 'string' ? log : JSON.stringify(log))).join('\n'); + throw new Error(`Build failed with ${result.logs.length} error(s).\n${diagnostics}`); } const outputPath = './dist/index.js'; @@ -21,4 +22,17 @@ export async function buildStandaloneCli(): Promise { await Bun.write(outputPath, `#!/usr/bin/env bun\n${compiled}`); } -await buildStandaloneCli(); +async function main(): Promise { + await buildStandaloneCli(); +} + +const isEsmMain = typeof import.meta !== 'undefined' && import.meta.main; +const cjs = globalThis as unknown as { require?: { main?: unknown }; module?: unknown }; +const isCjsMain = cjs.require?.main !== undefined && cjs.require.main === cjs.module; + +if (isEsmMain || isCjsMain) { + main().catch((error) => { + console.error('Build failed:', error); + process.exit(1); + }); +} diff --git a/betterbase/packages/cli/src/commands/init.ts b/betterbase/packages/cli/src/commands/init.ts index 6a856c8..5859b89 100644 --- a/betterbase/packages/cli/src/commands/init.ts +++ b/betterbase/packages/cli/src/commands/init.ts @@ -1,4 +1,4 @@ -import { mkdir, writeFile } from 'node:fs/promises'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { z } from 'zod'; import * as logger from '../utils/logger'; @@ -63,14 +63,10 @@ async function initializeGitRepository(projectPath: string): Promise { function buildPackageJson(projectName: string, databaseMode: DatabaseMode, useAuth: boolean): string { const dependencies: Record = { hono: '^4.11.9', - 'drizzle-orm': '^0.36.4', + 'drizzle-orm': '^0.44.5', zod: '^3.25.76', }; - if (databaseMode === 'local') { - dependencies['better-sqlite3'] = '^11.7.0'; - } - if (databaseMode === 'turso') { dependencies['@libsql/client'] = '^0.14.0'; } @@ -90,12 +86,12 @@ function buildPackageJson(projectName: string, databaseMode: DatabaseMode, useAu scripts: { dev: 'bun run src/index.ts', 'db:generate': 'drizzle-kit generate', - 'db:push': 'drizzle-kit push', + 'db:push': 'bun run src/db/migrate.ts', }, dependencies, devDependencies: { '@types/bun': '^1.3.9', - 'drizzle-kit': '^0.27.2', + 'drizzle-kit': '^0.31.4', typescript: '^5.9.3', }, }; @@ -110,6 +106,14 @@ function buildDrizzleConfig(databaseMode: DatabaseMode): string { turso: 'turso', }; + const databaseUrl: Record = { + local: "process.env.DATABASE_URL || 'file:local.db'", + neon: "process.env.DATABASE_URL || 'postgres://localhost'", + turso: "process.env.DATABASE_URL || 'libsql://localhost'", + }; + + const tursoAuthTokenLine = databaseMode === 'turso' ? "\n authToken: process.env.TURSO_AUTH_TOKEN || ''," : ''; + return `import { defineConfig } from 'drizzle-kit'; export default defineConfig({ @@ -117,13 +121,13 @@ export default defineConfig({ out: './drizzle', dialect: '${dialect[databaseMode]}', dbCredentials: { - url: process.env.DATABASE_URL || 'file:local.db', + url: ${databaseUrl[databaseMode]},${tursoAuthTokenLine} }, }); `; } -function buildSchema(databaseMode: DatabaseMode): string { +async function buildSchema(databaseMode: DatabaseMode): Promise { if (databaseMode === 'neon') { return `import { integer, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'; @@ -136,14 +140,133 @@ export const users = pgTable('users', { `; } - return `import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; + return `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +/** + * Adds created_at and updated_at timestamp columns. + * Note: .$onUpdate(() => new Date()) runs when updates go through Drizzle. + * For raw SQL writes, add a DB trigger if you need automatic updated_at changes. + */ +export const timestamps = { + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }) + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), +}; + +/** + * UUID primary-key helper. + */ +export const uuid = (name = 'id') => + text(name) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()); + +/** + * Soft-delete helper. + */ +export const softDelete = { + deletedAt: integer('deleted_at', { mode: 'timestamp' }), +}; + +/** + * Shared status enum helper. + */ +export const statusEnum = (name = 'status') => + text(name, { enum: ['active', 'inactive', 'pending'] }).default('active'); + +/** + * Currency helper stored as integer cents. + */ +export const moneyColumn = (name: string) => integer(name).notNull().default(0); + +/** + * JSON text helper with type support. + */ +export const jsonColumn = (name: string) => text(name, { mode: 'json' }).$type(); export const users = sqliteTable('users', { - id: integer('id').primaryKey({ autoIncrement: true }), + id: uuid(), email: text('email').notNull().unique(), name: text('name'), - createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + status: statusEnum(), + ...timestamps, + ...softDelete, +}); + +export const posts = sqliteTable('posts', { + id: uuid(), + title: text('title').notNull(), + content: text('content'), + userId: text('user_id').references(() => users.id), + ...timestamps, +}); +`; +} + +function buildMigrateScript(databaseMode: DatabaseMode): string { + if (databaseMode === 'neon') { + return `import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, }); + +const db = drizzle(pool); + +try { + await migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} finally { + await pool.end(); +} +`; + } + + if (databaseMode === 'turso') { + return `import { createClient } from '@libsql/client'; +import { drizzle } from 'drizzle-orm/libsql'; +import { migrate } from 'drizzle-orm/libsql/migrator'; + +const client = createClient({ + url: process.env.DATABASE_URL || 'file:local.db', + authToken: process.env.TURSO_AUTH_TOKEN, +}); + +const db = drizzle(client); + +try { + await migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} +`; + } + + return `import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; + +try { + const sqlite = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); + const db = drizzle(sqlite); + + migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} `; } @@ -168,6 +291,7 @@ import * as schema from './schema'; const client = createClient({ url: process.env.DATABASE_URL || 'file:local.db', + authToken: process.env.TURSO_AUTH_TOKEN, }); export const db = drizzle(client, { schema }); @@ -178,7 +302,7 @@ export const db = drizzle(client, { schema }); import { drizzle } from 'drizzle-orm/bun-sqlite'; import * as schema from './schema'; -const client = new Database('local.db', { create: true }); +const client = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); export const db = drizzle(client, { schema }); `; @@ -207,6 +331,38 @@ Generated with BetterBase CLI. `; } +function buildRoutesIndex(): string { + return `import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { HTTPException } from 'hono/http-exception'; +import { healthRoute } from './health'; +import { usersRoute } from './users'; + +export default function registerRoutes(app: Hono): void { + app.use('*', cors()); + app.use('*', logger()); + + app.onError((err, c) => { + const isHttpError = err instanceof HTTPException; + const showDetailedError = process.env.NODE_ENV === 'development' || isHttpError; + + return c.json( + { + error: showDetailedError ? err.message : 'Internal Server Error', + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: isHttpError ? (err as { cause?: unknown }).cause ?? null : null, + }, + isHttpError ? err.status : 500, + ); + }); + + app.route('/health', healthRoute); + app.route('/api/users', usersRoute); +} +`; +} + async function writeProjectFiles( projectPath: string, projectName: string, @@ -223,7 +379,7 @@ async function writeProjectFiles( `export default { mode: '${databaseMode}', database: { - local: 'sqlite://local.db', + local: 'local.db', production: process.env.DATABASE_URL, }, auth: { @@ -234,7 +390,6 @@ async function writeProjectFiles( ); await writeFile(path.join(projectPath, 'drizzle.config.ts'), buildDrizzleConfig(databaseMode)); - await writeFile(path.join(projectPath, 'package.json'), buildPackageJson(projectName, databaseMode, useAuth)); await writeFile( @@ -245,7 +400,6 @@ async function writeProjectFiles( "module": "ESNext", "moduleResolution": "Bundler", "strict": true, - "noImplicitAny": true, "esModuleInterop": true, "types": ["bun"], "skipLibCheck": true @@ -258,6 +412,8 @@ async function writeProjectFiles( await writeFile( path.join(projectPath, '.env.example'), `DATABASE_URL= +DB_PATH=local.db +TURSO_AUTH_TOKEN= NODE_ENV=development PORT=3000 `, @@ -268,100 +424,150 @@ PORT=3000 `node_modules bun.lockb .env +.env.* +!.env.example local.db .drizzle `, ); await writeFile(path.join(projectPath, 'README.md'), buildReadme(projectName)); - - await writeFile(path.join(projectPath, 'src/db/schema.ts'), buildSchema(databaseMode)); - + await writeFile(path.join(projectPath, 'src/db/schema.ts'), await buildSchema(databaseMode)); await writeFile(path.join(projectPath, 'src/db/index.ts'), buildDbIndex(databaseMode)); + await writeFile(path.join(projectPath, 'src/db/migrate.ts'), buildMigrateScript(databaseMode)); + await writeFile( path.join(projectPath, 'src/routes/health.ts'), - `import { Hono } from 'hono'; + `import { sql } from 'drizzle-orm'; +import { Hono } from 'hono'; +import { db } from '../db'; export const healthRoute = new Hono(); -healthRoute.get('/', (c) => { - return c.json({ - status: 'healthy', - database: 'connected', - timestamp: new Date().toISOString(), - }); +healthRoute.get('/', async (c) => { + try { + await db.run(sql\`select 1\`); + + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); + } catch { + return c.json( + { + status: 'unhealthy', + database: 'disconnected', + timestamp: new Date().toISOString(), + }, + 503, + ); + } }); `, ); await writeFile( - path.join(projectPath, 'src/routes/index.ts'), + path.join(projectPath, 'src/middleware/validation.ts'), + `import { HTTPException } from 'hono/http-exception'; +import type { ZodType } from 'zod'; + +export function parseBody(schema: ZodType, body: unknown): T { + const result = schema.safeParse(body); + + if (!result.success) { + throw new HTTPException(400, { + message: 'Validation failed', + cause: { + errors: result.error.issues.map((issue) => ({ + path: issue.path.join('.'), + message: issue.message, + code: issue.code, + })), + }, + }); + } + + return result.data; +} +`, + ); + + await writeFile( + path.join(projectPath, 'src/routes/users.ts'), `import { Hono } from 'hono'; -import { cors } from 'hono/cors'; -import { logger } from 'hono/logger'; import { HTTPException } from 'hono/http-exception'; +import { z } from 'zod'; import { db } from '../db'; import { users } from '../db/schema'; -import { healthRoute } from './health'; +import { parseBody } from '../middleware/validation'; -const app = new Hono(); - -app.use('*', cors()); -app.use('*', logger()); -app.use('*', async (c, next) => { - const start = performance.now(); - await next(); - const duration = (performance.now() - start).toFixed(2); - console.log(\`⏱ \${c.req.method} \${c.req.path} - \${duration}ms\`); +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), }); -app.onError((err, c) => { - console.error('Error:', err); - return c.json( - { - error: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, - details: err instanceof HTTPException ? (err as { cause?: unknown }).cause ?? null : null, - }, - err instanceof HTTPException ? err.status : 500, - ); -}); +export const usersRoute = new Hono(); -app.route('/health', healthRoute); - -app.get('/api/users', async (c) => { +usersRoute.get('/', async (c) => { const allUsers = await db.select().from(users); return c.json({ users: allUsers }); }); +usersRoute.post('/', async (c) => { + try { + const body = await c.req.json(); + const parsed = parseBody(createUserSchema, body); + + // TODO: persist parsed user via db.insert(users) or a dedicated UsersService. + return c.json({ + message: 'User payload validated (not persisted)', + user: parsed, + }); + } catch (error) { + if (error instanceof HTTPException) { + throw error; + } + + if (error instanceof SyntaxError) { + throw new HTTPException(400, { message: 'Malformed JSON body' }); + } + + throw error; + } +}); +`, + ); + + await writeFile(path.join(projectPath, 'src/routes/index.ts'), buildRoutesIndex()); + + await writeFile( + path.join(projectPath, 'src/index.ts'), + `import { Hono } from 'hono'; +import registerRoutes from './routes'; + +const app = new Hono(); +registerRoutes(app); + const server = Bun.serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000), development: process.env.NODE_ENV === 'development', }); -console.log('\x1b[32m🚀 BetterBase dev server started\x1b[0m'); -console.log(\`\x1b[36m→ URL:\x1b[0m http://localhost:\${server.port}\`); -console.log('\x1b[35m→ Routes:\x1b[0m'); -console.log(' GET /health'); -console.log(' GET /api/users'); +console.log(\`🚀 Server running at http://localhost:\${server.port}\`); +for (const route of app.routes) { + console.log(\` \${route.method} \${route.path}\`); +} process.on('SIGTERM', () => { - console.log('SIGTERM received, closing server...'); server.stop(); }); process.on('SIGINT', () => { - console.log('SIGINT received, closing server...'); server.stop(); }); -`, - ); - - await writeFile( - path.join(projectPath, 'src/index.ts'), - `import server from './routes/index'; export default server; `, @@ -418,8 +624,11 @@ export async function runInitCommand(rawOptions: InitCommandOptions): Promise; export async function runMigrateCommand(rawOptions: MigrateCommandOptions): Promise { const options = migrateOptionsSchema.parse(rawOptions); - logger.info('Analyzing schema changes...'); - const shouldContinue = options.destructive === true - ? await prompts.confirm({ - message: 'Destructive changes detected. Continue?', + ? true + : await prompts.confirm({ + message: 'This migration may include destructive changes. Continue?', initial: false, - }) - : true; + }); if (!shouldContinue) { logger.warn('Migration cancelled by user.'); return; } - logger.success('Migration flow completed (placeholder).'); + logger.info('Analyzing migration plan...'); + logger.success('Migration scaffold complete. (Placeholder implementation)'); } diff --git a/betterbase/packages/cli/src/index.ts b/betterbase/packages/cli/src/index.ts index e2da69b..e0136da 100644 --- a/betterbase/packages/cli/src/index.ts +++ b/betterbase/packages/cli/src/index.ts @@ -1,4 +1,4 @@ -import { Command } from 'commander'; +import { Command, CommanderError } from 'commander'; import { runInitCommand } from './commands/init'; import { runMigrateCommand } from './commands/migrate'; import * as logger from './utils/logger'; @@ -13,7 +13,8 @@ export function createProgram(): Command { program .name('bb') .description('BetterBase CLI') - .version(packageJson.version, '-v, --version', 'display the CLI version'); + .version(packageJson.version, '-v, --version', 'display the CLI version') + .exitOverride(); program .command('init') @@ -39,7 +40,16 @@ export function createProgram(): Command { */ export async function runCli(argv: string[] = process.argv): Promise { const program = createProgram(); - await program.parseAsync(argv); + + try { + await program.parseAsync(argv); + } catch (err) { + if (err instanceof CommanderError && (err.code === 'commander.helpDisplayed' || err.code === 'commander.version')) { + return; + } + + throw err; + } } if (import.meta.main) { diff --git a/betterbase/packages/cli/src/utils/logger.ts b/betterbase/packages/cli/src/utils/logger.ts index fe71756..0bbc131 100644 --- a/betterbase/packages/cli/src/utils/logger.ts +++ b/betterbase/packages/cli/src/utils/logger.ts @@ -1,17 +1,17 @@ import chalk from 'chalk'; /** - * Print an informational message to stdout. + * Print an informational message to stderr. */ export function info(message: string): void { - console.log(chalk.blue(`ℹ ${message}`)); + console.error(chalk.blue(`ℹ ${message}`)); } /** - * Print a warning message to stdout. + * Print a warning message to stderr. */ export function warn(message: string): void { - console.log(chalk.yellow(`⚠ ${message}`)); + console.warn(chalk.yellow(`⚠ ${message}`)); } /** @@ -22,8 +22,8 @@ export function error(message: string): void { } /** - * Print a success message to stdout. + * Print a success message to stderr. */ export function success(message: string): void { - console.log(chalk.green(`✔ ${message}`)); + console.error(chalk.green(`✔ ${message}`)); } diff --git a/betterbase/packages/cli/src/utils/prompts.ts b/betterbase/packages/cli/src/utils/prompts.ts index 591236f..a1f2d0e 100644 --- a/betterbase/packages/cli/src/utils/prompts.ts +++ b/betterbase/packages/cli/src/utils/prompts.ts @@ -16,11 +16,19 @@ const selectOptionSchema = z.object({ value: z.string().min(1), }); -const selectOptionsSchema = z.object({ - message: z.string().min(1), - choices: z.array(selectOptionSchema).min(1), - initial: z.string().optional(), -}); +const selectOptionsSchema = z + .object({ + message: z.string().min(1), + choices: z.array(selectOptionSchema).min(1), + initial: z.string().optional(), + }) + .refine( + ({ choices, initial }) => initial === undefined || choices.some((choice) => choice.value === initial), + { + message: 'Select initial value must match one of the choice values.', + path: ['initial'], + }, + ); /** * Prompt for text input. diff --git a/betterbase/packages/cli/test/smoke.test.ts b/betterbase/packages/cli/test/smoke.test.ts index aef00ac..f082470 100644 --- a/betterbase/packages/cli/test/smoke.test.ts +++ b/betterbase/packages/cli/test/smoke.test.ts @@ -13,4 +13,10 @@ describe('cli', () => { expect(init).toBeDefined(); expect(init?.registeredArguments[0]?.name()).toBe('project-name'); }); + + test('registers migrate command', () => { + const program = createProgram(); + const migrate = program.commands.find((command) => command.name() === 'migrate'); + expect(migrate).toBeDefined(); + }); }); diff --git a/betterbase/templates/base/betterbase.config.ts b/betterbase/templates/base/betterbase.config.ts index 31181e6..361834c 100644 --- a/betterbase/templates/base/betterbase.config.ts +++ b/betterbase/templates/base/betterbase.config.ts @@ -16,7 +16,7 @@ export type BetterBaseConfig = z.infer; export const betterbaseConfig: BetterBaseConfig = BetterBaseConfigSchema.parse({ mode: 'local', database: { - local: 'sqlite://local.db', + local: 'local.db', production: null, }, auth: { diff --git a/betterbase/templates/base/bun.lock b/betterbase/templates/base/bun.lock index d096091..57b6cd8 100644 --- a/betterbase/templates/base/bun.lock +++ b/betterbase/templates/base/bun.lock @@ -4,14 +4,14 @@ "": { "name": "betterbase-base-template", "dependencies": { - "drizzle-orm": "^0.36.4", + "drizzle-orm": "^0.44.5", "hono": "^4.6.10", "zod": "^3.23.8", }, "devDependencies": { "@types/bun": "^1.3.9", - "drizzle-kit": "^0.27.2", - "typescript": "^5.6.0", + "drizzle-kit": "^0.31.4", + "typescript": "^5.9.3", }, }, }, @@ -22,86 +22,168 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "drizzle-kit": ["drizzle-kit@0.27.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-F6cFZ1wxa9XzFyeeQsp/0/lIzUbDuQjS8/njpYBDWa+wdWmXuY+Z/X2hHFK/9PGHZkv3c9mER+mVWfKlp/B6Vw=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "drizzle-orm": ["drizzle-orm@0.36.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA=="], + "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], - "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], diff --git a/betterbase/templates/base/package.json b/betterbase/templates/base/package.json index 3144f6d..e631611 100644 --- a/betterbase/templates/base/package.json +++ b/betterbase/templates/base/package.json @@ -3,19 +3,19 @@ "private": true, "type": "module", "scripts": { - "dev": "bun --hot run src/routes/index.ts", + "dev": "bun --hot run src/index.ts", "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", + "db:push": "bun run src/db/migrate.ts", "typecheck": "tsc --noEmit" }, "dependencies": { "hono": "^4.6.10", "zod": "^3.23.8", - "drizzle-orm": "^0.36.4" + "drizzle-orm": "^0.44.5" }, "devDependencies": { "@types/bun": "^1.3.9", - "drizzle-kit": "^0.27.2", - "typescript": "^5.6.0" + "drizzle-kit": "^0.31.4", + "typescript": "^5.9.3" } } diff --git a/betterbase/templates/base/src/db/index.ts b/betterbase/templates/base/src/db/index.ts index e2a72b6..ddbbc3f 100644 --- a/betterbase/templates/base/src/db/index.ts +++ b/betterbase/templates/base/src/db/index.ts @@ -2,7 +2,7 @@ import { Database } from 'bun:sqlite'; import { drizzle } from 'drizzle-orm/bun-sqlite'; import * as schema from './schema'; -const dbPath = process.env.DB_PATH ?? Bun.env.DB_PATH ?? 'local.db'; +const dbPath = process.env.DB_PATH ?? 'local.db'; const sqlite = new Database(dbPath, { create: true }); export const db = drizzle(sqlite, { schema }); diff --git a/betterbase/templates/base/src/db/migrate.ts b/betterbase/templates/base/src/db/migrate.ts new file mode 100644 index 0000000..c590040 --- /dev/null +++ b/betterbase/templates/base/src/db/migrate.ts @@ -0,0 +1,15 @@ +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; + +try { + const sqlite = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); + const db = drizzle(sqlite); + + migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} diff --git a/betterbase/templates/base/src/db/schema.ts b/betterbase/templates/base/src/db/schema.ts index 3a7f526..ef5c131 100644 --- a/betterbase/templates/base/src/db/schema.ts +++ b/betterbase/templates/base/src/db/schema.ts @@ -1,14 +1,69 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +/** + * Adds created_at and updated_at timestamp columns. + * created_at is set on insert and updated_at is refreshed on updates. + * Note: .$onUpdate(() => new Date()) applies when updates go through Drizzle. + * Raw SQL writes will not auto-update this value without a DB trigger. + * + * @example + * export const users = sqliteTable('users', { + * id: uuid(), + * email: text('email'), + * ...timestamps, + * }); + */ +export const timestamps = { + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }) + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), +}; + +/** + * UUID primary-key helper. + */ +export const uuid = (name = 'id') => + text(name) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()); + +/** + * Soft-delete helper. + */ +export const softDelete = { + deletedAt: integer('deleted_at', { mode: 'timestamp' }), +}; + +/** + * Shared status enum helper. + */ +export const statusEnum = (name = 'status') => + text(name, { enum: ['active', 'inactive', 'pending'] }).default('active'); + +/** + * Currency helper stored as integer cents. + */ +export const moneyColumn = (name: string) => integer(name).notNull().default(0); + +/** + * JSON text helper with type support. + */ +export const jsonColumn = (name: string) => text(name, { mode: 'json' }).$type(); + export const users = sqliteTable('users', { - id: integer('id').primaryKey({ autoIncrement: true }), + id: uuid(), email: text('email').notNull().unique(), - name: text('name').notNull(), - createdAt: integer('created_at', { mode: 'timestamp_ms' }) - .$defaultFn(() => new Date()) - .notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) - .$defaultFn(() => new Date()) - .$onUpdate(() => new Date()) - .notNull(), + name: text('name'), + status: statusEnum(), + ...timestamps, + ...softDelete, +}); + +export const posts = sqliteTable('posts', { + id: uuid(), + title: text('title').notNull(), + content: text('content'), + userId: text('user_id').references(() => users.id), + ...timestamps, }); diff --git a/betterbase/templates/base/src/index.ts b/betterbase/templates/base/src/index.ts index 80be963..4065ba5 100644 --- a/betterbase/templates/base/src/index.ts +++ b/betterbase/templates/base/src/index.ts @@ -1 +1,26 @@ -import './routes/index'; +import { Hono } from 'hono'; +import { registerRoutes } from './routes'; + +const app = new Hono(); +registerRoutes(app); + +const server = Bun.serve({ + fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + development: process.env.NODE_ENV === 'development', +}); + +console.log(`🚀 Server running at http://localhost:${server.port}`); +for (const route of app.routes) { + console.log(` ${route.method} ${route.path}`); +} + +process.on('SIGTERM', () => { + server.stop(); +}); + +process.on('SIGINT', () => { + server.stop(); +}); + +export { app, server }; diff --git a/betterbase/templates/base/src/middleware/validation.ts b/betterbase/templates/base/src/middleware/validation.ts index 2330e7c..98950c7 100644 --- a/betterbase/templates/base/src/middleware/validation.ts +++ b/betterbase/templates/base/src/middleware/validation.ts @@ -1,6 +1,5 @@ import { HTTPException } from 'hono/http-exception'; import type { ZodType } from 'zod'; -import { z } from 'zod'; export function parseBody(schema: ZodType, body: unknown): T { const result = schema.safeParse(body); @@ -20,9 +19,3 @@ export function parseBody(schema: ZodType, body: unknown): T { return result.data; } - -// TODO: Placeholder schema for scaffolded user-creation routes. -export const createUserSchema = z.object({ - email: z.string().email(), - name: z.string().min(1), -}); diff --git a/betterbase/templates/base/src/routes/health.ts b/betterbase/templates/base/src/routes/health.ts index c8995ca..85c2add 100644 --- a/betterbase/templates/base/src/routes/health.ts +++ b/betterbase/templates/base/src/routes/health.ts @@ -1,11 +1,26 @@ +import { sql } from 'drizzle-orm'; import { Hono } from 'hono'; +import { db } from '../db'; export const healthRoute = new Hono(); -healthRoute.get('/', (c) => { - return c.json({ - status: 'healthy', - database: 'connected', - timestamp: new Date().toISOString(), - }); +healthRoute.get('/', async (c) => { + try { + await db.run(sql`select 1`); + + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); + } catch { + return c.json( + { + status: 'unhealthy', + database: 'disconnected', + timestamp: new Date().toISOString(), + }, + 503, + ); + } }); diff --git a/betterbase/templates/base/src/routes/index.ts b/betterbase/templates/base/src/routes/index.ts index d5a0174..64a9e83 100644 --- a/betterbase/templates/base/src/routes/index.ts +++ b/betterbase/templates/base/src/routes/index.ts @@ -2,61 +2,27 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { HTTPException } from 'hono/http-exception'; -import { db } from '../db'; -import { users } from '../db/schema'; import { healthRoute } from './health'; import { usersRoute } from './users'; -const app = new Hono(); - -app.use('*', cors()); -app.use('*', logger()); -app.use('*', async (c, next) => { - const start = performance.now(); - await next(); - const duration = (performance.now() - start).toFixed(2); - console.log(`⏱ ${c.req.method} ${c.req.path} - ${duration}ms`); -}); - -app.onError((err, c) => { - console.error('Error:', err); - return c.json( - { - error: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, - details: err instanceof HTTPException ? (err as { cause?: unknown }).cause ?? null : null, - }, - err instanceof HTTPException ? err.status : 500, - ); -}); - -app.route('/health', healthRoute); -app.route('/users', usersRoute); - -app.get('/api/users', async (c) => { - const allUsers = await db.select().from(users); - return c.json({ users: allUsers }); -}); - -const server = Bun.serve({ - fetch: app.fetch, - port: Number(process.env.PORT ?? 3000), - development: process.env.NODE_ENV === 'development', -}); - -console.log('\x1b[32m🚀 BetterBase dev server started\x1b[0m'); -console.log(`\x1b[36m→ URL:\x1b[0m http://localhost:${server.port}`); -console.log('\x1b[35m→ Routes:\x1b[0m'); -console.log(' GET /health'); -console.log(' GET /api/users'); -console.log(' POST /users'); - -process.on('SIGTERM', () => { - console.log('SIGTERM received, closing server...'); - server.stop(); -}); - -process.on('SIGINT', () => { - console.log('SIGINT received, closing server...'); - server.stop(); -}); +export function registerRoutes(app: Hono): void { + app.use('*', cors()); + app.use('*', logger()); + + app.onError((err, c) => { + const isHttpError = err instanceof HTTPException; + const showDetailedError = process.env.NODE_ENV === 'development' || isHttpError; + + return c.json( + { + error: showDetailedError ? err.message : 'Internal Server Error', + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: isHttpError ? (err as { cause?: unknown }).cause ?? null : null, + }, + isHttpError ? err.status : 500, + ); + }); + + app.route('/health', healthRoute); + app.route('/api/users', usersRoute); +} diff --git a/betterbase/templates/base/src/routes/users.ts b/betterbase/templates/base/src/routes/users.ts index 8bf3acf..29f17c6 100644 --- a/betterbase/templates/base/src/routes/users.ts +++ b/betterbase/templates/base/src/routes/users.ts @@ -1,21 +1,32 @@ import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; -import { createUserSchema, parseBody } from '../middleware/validation'; +import { z } from 'zod'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { parseBody } from '../middleware/validation'; -const usersRoute = new Hono(); +export const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), +}); + +export const usersRoute = new Hono(); + +usersRoute.get('/', async (c) => { + const allUsers = await db.select().from(users); + return c.json({ users: allUsers }); +}); usersRoute.post('/', async (c) => { try { const body = await c.req.json(); const parsed = parseBody(createUserSchema, body); - return c.json( - { - message: 'User payload validated', - user: parsed, - }, - 201, - ); + // TODO: persist parsed user via db.insert(users) or a dedicated UsersService. + return c.json({ + message: 'User payload validated (not persisted)', + user: parsed, + }); } catch (error) { if (error instanceof HTTPException) { throw error; @@ -25,8 +36,6 @@ usersRoute.post('/', async (c) => { throw new HTTPException(400, { message: 'Malformed JSON body' }); } - throw new HTTPException(400, { message: 'Invalid request body' }); + throw error; } }); - -export { usersRoute };