feat: add PostgreSQL persistence for permanent shareable links

- Add PostgreSQL service to docker-compose with health checks
- Add migrate service that runs prisma migrate deploy on startup
- Integrate Prisma client in worker: checks for existing generations
  (same repo+commit) before regenerating, writes to Postgres on completion
- Update /api/docs/[id] with Redis → PostgreSQL fallback + cache repopulation
- Update Dockerfile: prisma generate in build, copy Prisma engine to worker/web
- Add @codeboard/database dependency to web and worker packages
- Add initial SQL migration for Generation and User tables
- Change removeOnComplete to { age: 3600 } for job retention
This commit is contained in:
Vectry
2026-02-09 20:23:41 +00:00
parent a49f05e8df
commit 30bfd88075
11 changed files with 170 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const config = {
transpilePackages: ["@codeboard/shared"],
transpilePackages: ["@codeboard/shared", "@codeboard/database"],
output: "standalone",
};

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@codeboard/shared": "*",
"@codeboard/database": "*",
"@tailwindcss/typography": "^0.5.19",
"bullmq": "^5.34.0",
"ioredis": "^5.4.0",

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { getRedis } from "@/lib/redis";
import { prisma } from "@codeboard/database";
export async function GET(
_request: Request,
@@ -8,13 +9,34 @@ export async function GET(
const { id } = await params;
const redis = getRedis();
const result = await redis.get(`codeboard:result:${id}`);
if (!result) {
let result = await redis.get(`codeboard:result:${id}`);
if (result) {
return NextResponse.json(JSON.parse(result));
}
const generation = await prisma.generation.findFirst({
where: { id }
});
if (!generation || !generation.result) {
return NextResponse.json(
{ error: "Documentation not found" },
{ status: 404 }
);
}
return NextResponse.json(JSON.parse(result));
const docs = generation.result as any;
docs.id = id;
docs.repoUrl = generation.repoUrl;
docs.repoName = generation.repoName;
await redis.set(
`codeboard:result:${id}`,
JSON.stringify(docs),
"EX",
86400
);
return NextResponse.json(docs);
}

View File

@@ -28,7 +28,7 @@ export async function POST(request: Request) {
const queue = getQueue();
await queue.add("generate", { repoUrl, generationId }, {
jobId: generationId,
removeOnComplete: true,
removeOnComplete: { age: 3600 },
removeOnFail: false,
});

View File

@@ -13,6 +13,7 @@
"@codeboard/parser": "*",
"@codeboard/llm": "*",
"@codeboard/diagrams": "*",
"@codeboard/database": "*",
"bullmq": "^5.34.0",
"ioredis": "^5.4.0",
"simple-git": "^3.27.0"

View File

@@ -1,5 +1,6 @@
import type { Job } from "bullmq";
import IORedis from "ioredis";
import { prisma } from "@codeboard/database";
import { cloneRepository } from "./jobs/clone.js";
import { parseRepository } from "./jobs/parse.js";
import { generateDocs } from "./jobs/generate.js";
@@ -38,6 +39,33 @@ export async function processGenerationJob(
try {
await updateProgress(generationId, "CLONING", 10, "Cloning repository...");
const cloneResult = await cloneRepository(repoUrl);
const commitHash = cloneResult.metadata.lastCommit;
const existingGeneration = await prisma.generation.findUnique({
where: {
repoUrl_commitHash: {
repoUrl,
commitHash
}
}
});
if (existingGeneration && existingGeneration.result) {
const docs = existingGeneration.result as any;
docs.id = generationId;
docs.repoUrl = repoUrl;
docs.repoName = existingGeneration.repoName;
await redis.set(
`codeboard:result:${generationId}`,
JSON.stringify(docs),
"EX",
86400
);
await updateProgress(generationId, "COMPLETED", 100, "Using cached documentation!");
return { generationId, duration: 0, repoName: existingGeneration.repoName, cached: true };
}
await updateProgress(
generationId,
@@ -72,9 +100,22 @@ export async function processGenerationJob(
86400
);
await prisma.generation.create({
data: {
id: generationId,
repoUrl,
repoName: cloneResult.metadata.name,
commitHash,
status: "COMPLETED",
progress: 100,
result: docs as any,
duration
}
});
await updateProgress(generationId, "COMPLETED", 100, "Done!");
return { generationId, duration, repoName: cloneResult.metadata.name };
return { generationId, duration, repoName: cloneResult.metadata.name, cached: false };
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
await updateProgress(generationId, "FAILED", 0, message);