Switch database from SQLite to PostgreSQL

Replace in-memory JSON file storage with Prisma ORM using PostgreSQL.
Update environment variables and Prisma schema accordingly.
This commit is contained in:
2026-05-19 14:19:01 +02:00
parent 06ab791755
commit 45cf61525e
10 changed files with 79 additions and 60 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
DATABASE_URL="file:./dev.db" DATABASE_URL="postgresql://postgres:postgres@localhost:5432/bikeapp?schema=public"
OLLAMA_URL="http://localhost:11434" OLLAMA_URL="http://localhost:11434"
OLLAMA_MODEL="llama3.1:8b" OLLAMA_MODEL="llama3.1:8b"
+3
View File
@@ -0,0 +1,3 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/bikeapp?schema=public"
OLLAMA_URL="http://localhost:11434"
OLLAMA_MODEL="llama3.1:8b"
+31 -1
View File
@@ -1 +1,31 @@
{"scripts":{"dev":"next dev","build":"next build","start":"next start","lint":"eslint src","db:seed":"tsx prisma/seed.ts"},"dependencies":{"@prisma/client":"^5.22.0","next":"^16.2.6","postcss":"^8.5.10","prisma":"^5.22.0","react":"^19.2.1","react-dom":"^19.2.1","sharp":"^0.33.5","zod":"^3.23.8"},"devDependencies":{"@types/node":"^20.17.6","@types/react":"^19.2.7","@types/react-dom":"^19.2.3","eslint":"^9.39.4","eslint-config-next":"^16.2.6","tsx":"^4.19.2","typescript":"^5.6.3"}} {
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint src",
"db:seed": "tsx prisma/seed.ts",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio",
"postinstall": "prisma generate"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"next": "^16.2.6",
"postcss": "^8.5.10",
"prisma": "^5.22.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"sharp": "^0.33.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.17.6",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.6",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ generator client {
} }
datasource db { datasource db {
provider = "sqlite" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export async function POST(request: Request) {
.filter((product): product is Product => Boolean(product)) .filter((product): product is Product => Boolean(product))
.map((product) => setupVerification[product.id]) .map((product) => setupVerification[product.id])
.filter(Boolean); .filter(Boolean);
const prefs = store.preferences(); const prefs = await store.preferences();
const prompt = [ const prompt = [
"You are a professional mountain bike suspension and tire setup tuner.", "You are a professional mountain bike suspension and tire setup tuner.",
"Combine manufacturer setup ranges, community setup patterns, and the rider's past preferences.", "Combine manufacturer setup ranges, community setup patterns, and the rider's past preferences.",
+2 -2
View File
@@ -21,7 +21,7 @@ const schema = z.object({
}); });
export async function GET() { export async function GET() {
return NextResponse.json(store.listBuilds()); return NextResponse.json(await store.listBuilds());
} }
export async function POST(request: Request) { export async function POST(request: Request) {
@@ -35,7 +35,7 @@ export async function POST(request: Request) {
rearTireId: body.rearTireId, rearTireId: body.rearTireId,
}; };
const setup = { ...buildSetupDefaults(selection, body.rider), ...body.setup }; const setup = { ...buildSetupDefaults(selection, body.rider), ...body.setup };
const build = store.createBuild({ const build = await store.createBuild({
name: body.name, name: body.name,
riderWeightKg: body.rider.weight, riderWeightKg: body.rider.weight,
discipline: body.rider.discipline, discipline: body.rider.discipline,
+1 -1
View File
@@ -19,6 +19,6 @@ const schema = z.object({
export async function POST(request: Request) { export async function POST(request: Request) {
const body = schema.parse(await request.json()); const body = schema.parse(await request.json());
const review = store.createReview(body); const review = await store.createReview(body);
return NextResponse.json(review, { status: 201 }); return NextResponse.json(review, { status: 201 });
} }
+31 -53
View File
@@ -1,58 +1,36 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { prisma } from "./prisma";
import path from "node:path";
type Database = { function parseSetup(build: any) {
builds: any[]; return {
reviews: any[]; ...build,
}; setup: build.setup ? JSON.parse(build.setup) : {},
};
const dbPath = path.join(process.cwd(), "data", "bike-app.json");
function readDb(): Database {
mkdirSync(path.dirname(dbPath), { recursive: true });
try {
return JSON.parse(readFileSync(dbPath, "utf8")) as Database;
} catch {
return { builds: [], reviews: [] };
}
}
function writeDb(data: Database) {
mkdirSync(path.dirname(dbPath), { recursive: true });
writeFileSync(dbPath, JSON.stringify(data, null, 2));
} }
export const store = { export const store = {
listBuilds() { async listBuilds() {
return readDb().builds.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))); const builds = await prisma.bikeBuild.findMany({
}, orderBy: { createdAt: "desc" },
createBuild(input: any) { });
const data = readDb(); return builds.map(parseSetup);
const build = { },
id: crypto.randomUUID(), async createBuild(input: any) {
...input, const build = await prisma.bikeBuild.create({
createdAt: new Date().toISOString(), data: {
}; ...input,
data.builds.unshift(build); setup: JSON.stringify(input.setup ?? {}),
writeDb(data); },
return build; });
}, return parseSetup(build);
createReview(input: any) { },
const data = readDb(); async createReview(input: any) {
const review = { return prisma.rideReview.create({ data: input });
id: crypto.randomUUID(), },
...input, async preferences() {
createdAt: new Date().toISOString(), const reviews = await prisma.rideReview.findMany();
}; return {
data.reviews.unshift(review); harshTendency: reviews.filter((review) => review.suspFeel >= 7).length,
writeDb(data); softTendency: reviews.filter((review) => review.suspFeel <= 3).length,
return review; };
}, },
preferences() {
const reviews = readDb().reviews;
return {
harshTendency: reviews.filter((review) => review.suspFeel >= 7).length,
softTendency: reviews.filter((review) => review.suspFeel <= 3).length,
};
},
}; };
+7
View File
@@ -0,0 +1,7 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
File diff suppressed because one or more lines are too long