diff --git a/bun.lock b/bun.lock index 4240ec4..228267e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "gearbox", "dependencies": { + "@electric-sql/pglite": "^0.4.3", "@hono/zod-validator": "^0.7.6", "@modelcontextprotocol/sdk": "^1.29.0", "@tailwindcss/vite": "^4.2.1", @@ -15,6 +16,7 @@ "framer-motion": "^12.38.0", "hono": "^4.12.8", "lucide-react": "^0.577.0", + "postgres": "^3.4.8", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.0", @@ -28,12 +30,10 @@ "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router-devtools": "^1.166.7", "@tanstack/router-plugin": "^1.166.9", - "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "better-sqlite3": "^12.8.0", "concurrently": "^9.1.2", "drizzle-kit": "^0.31.9", "vite": "^8.0.0", @@ -102,6 +102,8 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="], + "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], @@ -684,6 +686,8 @@ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + "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=="], "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index fcbc462..4c04208 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,10 +1,12 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ - out: "./drizzle", + out: "./drizzle-pg", schema: "./src/db/schema.ts", - dialect: "sqlite", + dialect: "postgresql", dbCredentials: { - url: process.env.DATABASE_PATH || "gearbox.db", + url: + process.env.DATABASE_URL || + "postgresql://gearbox:gearbox@localhost:5432/gearbox", }, }); diff --git a/package.json b/package.json index 281ba01..316e912 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,10 @@ "@tanstack/react-query-devtools": "^5.91.3", "@tanstack/react-router-devtools": "^1.166.7", "@tanstack/router-plugin": "^1.166.9", - "@types/better-sqlite3": "^7.6.13", "@types/bun": "latest", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "better-sqlite3": "^12.8.0", "concurrently": "^9.1.2", "drizzle-kit": "^0.31.9", "vite": "^8.0.0" @@ -35,6 +33,7 @@ "typescript": "^5.9.3" }, "dependencies": { + "@electric-sql/pglite": "^0.4.3", "@hono/zod-validator": "^0.7.6", "@modelcontextprotocol/sdk": "^1.29.0", "@tailwindcss/vite": "^4.2.1", @@ -45,6 +44,7 @@ "framer-motion": "^12.38.0", "hono": "^4.12.8", "lucide-react": "^0.577.0", + "postgres": "^3.4.8", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.0", diff --git a/src/db/index.ts b/src/db/index.ts index f50ccf4..f0424b3 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,9 +1,9 @@ -import { Database } from "bun:sqlite"; -import { drizzle } from "drizzle-orm/bun-sqlite"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; import * as schema from "./schema.ts"; -const sqlite = new Database(process.env.DATABASE_PATH || "gearbox.db"); -sqlite.run("PRAGMA journal_mode = WAL"); -sqlite.run("PRAGMA foreign_keys = ON"); - -export const db = drizzle(sqlite, { schema }); +const connectionString = + process.env.DATABASE_URL || + "postgresql://gearbox:gearbox@localhost:5432/gearbox"; +const queryClient = postgres(connectionString); +export const db = drizzle(queryClient, { schema }); diff --git a/src/db/migrate.ts b/src/db/migrate.ts index f7bd12d..e413ba6 100644 --- a/src/db/migrate.ts +++ b/src/db/migrate.ts @@ -1,13 +1,14 @@ -import { Database } from "bun:sqlite"; -import { drizzle } from "drizzle-orm/bun-sqlite"; -import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; -const sqlite = new Database(process.env.DATABASE_PATH || "gearbox.db"); -sqlite.run("PRAGMA journal_mode = WAL"); -sqlite.run("PRAGMA foreign_keys = ON"); +const connectionString = + process.env.DATABASE_URL || + "postgresql://gearbox:gearbox@localhost:5432/gearbox"; +const migrationClient = postgres(connectionString, { max: 1 }); +const db = drizzle(migrationClient); -const db = drizzle(sqlite); -migrate(db, { migrationsFolder: "./drizzle" }); +await migrate(db, { migrationsFolder: "./drizzle-pg" }); +await migrationClient.end(); -sqlite.close(); console.log("Migrations applied successfully"); diff --git a/src/db/schema.ts b/src/db/schema.ts index 9e7113b..013d487 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,18 +1,24 @@ -import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { + boolean, + doublePrecision, + integer, + pgTable, + serial, + text, + timestamp, +} from "drizzle-orm/pg-core"; -export const categories = sqliteTable("categories", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const categories = pgTable("categories", { + id: serial("id").primaryKey(), name: text("name").notNull().unique(), icon: text("icon").notNull().default("package"), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), }); -export const items = sqliteTable("items", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const items = pgTable("items", { + id: serial("id").primaryKey(), name: text("name").notNull(), - weightGrams: real("weight_grams"), + weightGrams: doublePrecision("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() @@ -22,37 +28,29 @@ export const items = sqliteTable("items", { imageFilename: text("image_filename"), imageSourceUrl: text("image_source_url"), quantity: integer("quantity").notNull().default(1), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), }); -export const threads = sqliteTable("threads", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const threads = pgTable("threads", { + id: serial("id").primaryKey(), name: text("name").notNull(), status: text("status").notNull().default("active"), resolvedCandidateId: integer("resolved_candidate_id"), categoryId: integer("category_id") .notNull() .references(() => categories.id), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), }); -export const threadCandidates = sqliteTable("thread_candidates", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const threadCandidates = pgTable("thread_candidates", { + id: serial("id").primaryKey(), threadId: integer("thread_id") .notNull() .references(() => threads.id, { onDelete: "cascade" }), name: text("name").notNull(), - weightGrams: real("weight_grams"), + weightGrams: doublePrecision("weight_grams"), priceCents: integer("price_cents"), categoryId: integer("category_id") .notNull() @@ -64,28 +62,20 @@ export const threadCandidates = sqliteTable("thread_candidates", { status: text("status").notNull().default("researching"), pros: text("pros"), cons: text("cons"), - sortOrder: real("sort_order").notNull().default(0), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + sortOrder: doublePrecision("sort_order").notNull().default(0), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), }); -export const setups = sqliteTable("setups", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const setups = pgTable("setups", { + id: serial("id").primaryKey(), name: text("name").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), - updatedAt: integer("updated_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow(), }); -export const setupItems = sqliteTable("setup_items", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const setupItems = pgTable("setup_items", { + id: serial("id").primaryKey(), setupId: integer("setup_id") .notNull() .references(() => setups.id, { onDelete: "cascade" }), @@ -95,69 +85,59 @@ export const setupItems = sqliteTable("setup_items", { classification: text("classification").notNull().default("base"), }); -export const settings = sqliteTable("settings", { +export const settings = pgTable("settings", { key: text("key").primaryKey(), value: text("value").notNull(), }); -export const users = sqliteTable("users", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const users = pgTable("users", { + id: serial("id").primaryKey(), username: text("username").notNull().unique(), passwordHash: text("password_hash").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), }); -export const sessions = sqliteTable("sessions", { +export const sessions = pgTable("sessions", { id: text("id").primaryKey(), userId: integer("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), + expiresAt: timestamp("expires_at").notNull(), }); -export const apiKeys = sqliteTable("api_keys", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const apiKeys = pgTable("api_keys", { + id: serial("id").primaryKey(), name: text("name").notNull(), keyHash: text("key_hash").notNull(), keyPrefix: text("key_prefix").notNull(), - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), }); -export const oauthClients = sqliteTable("oauth_clients", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const oauthClients = pgTable("oauth_clients", { + id: serial("id").primaryKey(), clientId: text("client_id").notNull().unique(), clientName: text("client_name"), redirectUris: text("redirect_uris").notNull(), // JSON array - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + createdAt: timestamp("created_at").notNull().defaultNow(), }); -export const oauthCodes = sqliteTable("oauth_codes", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const oauthCodes = pgTable("oauth_codes", { + id: serial("id").primaryKey(), code: text("code").notNull().unique(), clientId: text("client_id").notNull(), codeChallenge: text("code_challenge").notNull(), codeChallengeMethod: text("code_challenge_method").notNull().default("S256"), redirectUri: text("redirect_uri").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), - used: integer("used").notNull().default(0), + expiresAt: timestamp("expires_at").notNull(), + used: boolean("used").notNull().default(false), }); -export const oauthTokens = sqliteTable("oauth_tokens", { - id: integer("id").primaryKey({ autoIncrement: true }), +export const oauthTokens = pgTable("oauth_tokens", { + id: serial("id").primaryKey(), accessTokenHash: text("access_token_hash").notNull().unique(), refreshTokenHash: text("refresh_token_hash").notNull().unique(), clientId: text("client_id").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), // access token expiry - refreshExpiresAt: integer("refresh_expires_at", { - mode: "timestamp", - }).notNull(), // refresh token expiry - createdAt: integer("created_at", { mode: "timestamp" }) - .notNull() - .$defaultFn(() => new Date()), + expiresAt: timestamp("expires_at").notNull(), // access token expiry + refreshExpiresAt: timestamp("refresh_expires_at").notNull(), // refresh token expiry + createdAt: timestamp("created_at").notNull().defaultNow(), }); diff --git a/src/db/seed.ts b/src/db/seed.ts index c7cf900..4529f33 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -1,14 +1,12 @@ import { db } from "./index.ts"; import { categories } from "./schema.ts"; -export function seedDefaults() { - const existing = db.select().from(categories).all(); +export async function seedDefaults() { + const existing = await db.select().from(categories); if (existing.length === 0) { - db.insert(categories) - .values({ - name: "Uncategorized", - icon: "package", - }) - .run(); + await db.insert(categories).values({ + name: "Uncategorized", + icon: "package", + }); } }