Move and re-export all PDF parsing and TTS-related types from @openreader/compute-core/types, eliminating local type duplication. Update imports throughout the codebase to use the new centralized type module. Remove obsolete worker-contract file and update tsconfig paths and compute-core exports for the new types entry point. Refactor API routes and job logic to use the new type imports and shared parse-state healing utility. This streamlines type management and improves consistency between compute and app layers.
233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
import { db } from '@/db';
|
|
import { documents } from '@/db/schema';
|
|
import { requireAuthContext } from '@/lib/server/auth/auth';
|
|
import {
|
|
getParsedDocumentBlob,
|
|
getParsedDocumentBlobByKey,
|
|
isMissingBlobError,
|
|
isValidDocumentId,
|
|
} from '@/lib/server/documents/blobstore';
|
|
import { enqueueParsePdfJob } from '@/lib/server/jobs/parsePdfJob';
|
|
import {
|
|
normalizeParseStatus,
|
|
parseDocumentParseState,
|
|
stringifyDocumentParseState,
|
|
} from '@/lib/server/documents/parse-state';
|
|
import { healStaleDocumentParseState } from '@/lib/server/documents/parse-state-healing';
|
|
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
|
|
import { isS3Configured } from '@/lib/server/storage/s3';
|
|
import type { ParsedPdfDocument } from '@/types/parsed-pdf';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function s3NotConfiguredResponse(): NextResponse {
|
|
return NextResponse.json(
|
|
{ error: 'Documents storage is not configured. Set S3_* environment variables.' },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
function hasAnyParsedBlocks(doc: ParsedPdfDocument | null): boolean {
|
|
if (!doc || !Array.isArray(doc.pages)) return false;
|
|
return doc.pages.some((page) => Array.isArray(page.blocks) && page.blocks.length > 0);
|
|
}
|
|
|
|
export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
if (!isS3Configured()) return s3NotConfiguredResponse();
|
|
|
|
const authCtxOrRes = await requireAuthContext(req);
|
|
if (authCtxOrRes instanceof Response) return authCtxOrRes;
|
|
|
|
const params = await ctx.params;
|
|
const id = (params.id || '').trim().toLowerCase();
|
|
const retryFailed = req.nextUrl.searchParams.get('retry') === '1';
|
|
if (!isValidDocumentId(id)) {
|
|
return NextResponse.json({ error: 'Invalid document id' }, { status: 400 });
|
|
}
|
|
|
|
const testNamespace = getOpenReaderTestNamespace(req.headers);
|
|
const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace);
|
|
const storageUserId = authCtxOrRes.userId ?? unclaimedUserId;
|
|
const allowedUserIds = authCtxOrRes.authEnabled ? [storageUserId, unclaimedUserId] : [unclaimedUserId];
|
|
|
|
const rows = (await db
|
|
.select({
|
|
id: documents.id,
|
|
userId: documents.userId,
|
|
parseState: documents.parseState,
|
|
parsedJsonKey: documents.parsedJsonKey,
|
|
})
|
|
.from(documents)
|
|
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)))) as Array<{
|
|
id: string;
|
|
userId: string;
|
|
parseState: string | null;
|
|
parsedJsonKey: string | null;
|
|
}>;
|
|
|
|
const row = rows.find((candidate) => candidate.userId === storageUserId) ?? rows[0];
|
|
if (!row) {
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
}
|
|
|
|
let state = parseDocumentParseState(row.parseState);
|
|
state = await healStaleDocumentParseState({
|
|
documentId: id,
|
|
userId: row.userId,
|
|
state,
|
|
});
|
|
const effectiveStatus = normalizeParseStatus(state.status);
|
|
|
|
if (effectiveStatus === 'failed' && retryFailed) {
|
|
await db
|
|
.update(documents)
|
|
.set({
|
|
parseState: stringifyDocumentParseState({
|
|
status: 'pending',
|
|
progress: null,
|
|
updatedAt: Date.now(),
|
|
}),
|
|
})
|
|
.where(and(eq(documents.id, id), eq(documents.userId, row.userId)));
|
|
enqueueParsePdfJob({
|
|
documentId: id,
|
|
userId: row.userId,
|
|
namespace: testNamespace,
|
|
});
|
|
return NextResponse.json({ parseStatus: 'pending', parseProgress: null }, { status: 202 });
|
|
}
|
|
|
|
if (effectiveStatus !== 'ready') {
|
|
if (effectiveStatus === 'pending') {
|
|
enqueueParsePdfJob({
|
|
documentId: id,
|
|
userId: row.userId,
|
|
namespace: testNamespace,
|
|
});
|
|
}
|
|
return NextResponse.json({
|
|
parseStatus: effectiveStatus,
|
|
parseProgress: state.progress ?? null,
|
|
}, { status: 202 });
|
|
}
|
|
|
|
try {
|
|
const json = row.parsedJsonKey?.trim()
|
|
? await getParsedDocumentBlobByKey(row.parsedJsonKey)
|
|
: await getParsedDocumentBlob(id, testNamespace);
|
|
let parsedDoc: ParsedPdfDocument | null = null;
|
|
try {
|
|
parsedDoc = JSON.parse(Buffer.from(json).toString('utf8')) as ParsedPdfDocument;
|
|
} catch {
|
|
parsedDoc = null;
|
|
}
|
|
|
|
if (!hasAnyParsedBlocks(parsedDoc)) {
|
|
console.warn('[documents/parsed] parsed doc has no blocks', {
|
|
documentId: id,
|
|
userId: row.userId,
|
|
parsedJsonKey: row.parsedJsonKey,
|
|
});
|
|
}
|
|
|
|
return new NextResponse(new Uint8Array(json), {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'no-store',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (isMissingBlobError(error)) {
|
|
return NextResponse.json({ parseStatus: 'failed', error: 'Parsed document not found' }, { status: 404 });
|
|
}
|
|
throw error;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error reading parsed PDF:', error);
|
|
return NextResponse.json({ error: 'Failed to read parsed PDF' }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
if (!isS3Configured()) return s3NotConfiguredResponse();
|
|
|
|
const authCtxOrRes = await requireAuthContext(req);
|
|
if (authCtxOrRes instanceof Response) return authCtxOrRes;
|
|
|
|
const params = await ctx.params;
|
|
const id = (params.id || '').trim().toLowerCase();
|
|
if (!isValidDocumentId(id)) {
|
|
return NextResponse.json({ error: 'Invalid document id' }, { status: 400 });
|
|
}
|
|
|
|
const testNamespace = getOpenReaderTestNamespace(req.headers);
|
|
const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace);
|
|
const storageUserId = authCtxOrRes.userId ?? unclaimedUserId;
|
|
const allowedUserIds = authCtxOrRes.authEnabled ? [storageUserId, unclaimedUserId] : [unclaimedUserId];
|
|
|
|
const rows = (await db
|
|
.select({
|
|
id: documents.id,
|
|
userId: documents.userId,
|
|
parseState: documents.parseState,
|
|
parsedJsonKey: documents.parsedJsonKey,
|
|
})
|
|
.from(documents)
|
|
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)))) as Array<{
|
|
id: string;
|
|
userId: string;
|
|
parseState: string | null;
|
|
parsedJsonKey: string | null;
|
|
}>;
|
|
|
|
const row = rows.find((candidate) => candidate.userId === storageUserId) ?? rows[0];
|
|
if (!row) {
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
}
|
|
|
|
let state = parseDocumentParseState(row.parseState);
|
|
state = await healStaleDocumentParseState({
|
|
documentId: id,
|
|
userId: row.userId,
|
|
state,
|
|
});
|
|
const effectiveStatus = normalizeParseStatus(state.status);
|
|
|
|
if (effectiveStatus !== 'running') {
|
|
await db
|
|
.update(documents)
|
|
.set({
|
|
parseState: stringifyDocumentParseState({
|
|
status: 'pending',
|
|
progress: null,
|
|
updatedAt: Date.now(),
|
|
}),
|
|
})
|
|
.where(and(eq(documents.id, id), eq(documents.userId, row.userId)));
|
|
}
|
|
|
|
enqueueParsePdfJob({
|
|
documentId: id,
|
|
userId: row.userId,
|
|
namespace: testNamespace,
|
|
forceToken: randomUUID(),
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
parseStatus: effectiveStatus === 'running' ? 'running' : 'pending',
|
|
parseProgress: effectiveStatus === 'running' ? (state.progress ?? null) : null,
|
|
},
|
|
{ status: 202 },
|
|
);
|
|
} catch (error) {
|
|
console.error('Error forcing parsed PDF refresh:', error);
|
|
return NextResponse.json({ error: 'Failed to force parsed PDF refresh' }, { status: 500 });
|
|
}
|
|
}
|