Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@prisma/adapter-pg": "7.0.1",
"@prisma/client": "7.0.1",
"axios": "1.13.2",
"body-parser": "^1.20.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"cookie-parser": "^1.4.7",
Expand Down
79 changes: 77 additions & 2 deletions apps/backend-services/src/document/document.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ import {
NotFoundException,
Param,
Req,
Res,
} from "@nestjs/common";
import { Request } from "express";
import { Request, Response } from "express";
import { readFile } from "fs/promises";
import { join } from "path";
import { OcrResult } from "@/generated/client";
import { Roles } from "../auth/roles.decorator";
import { DatabaseService, DocumentData } from "../database/database.service";
Expand Down Expand Up @@ -93,18 +96,33 @@ export class DocumentController {
this.logger.debug(`Document ID: ${documentId}`);

try {
// First check if document exists and its status
const document = await this.databaseService.findDocument(documentId);
if (document) {
this.logger.debug(`Document status: ${document.status}`);
this.logger.debug(`Document created: ${document.created_at}`);
if (document.apim_request_id) {
this.logger.debug(`APIM Request ID: ${document.apim_request_id}`);
}
} else {
this.logger.warn(`Document not found: ${documentId}`);
throw new NotFoundException(`Document not found: ${documentId}`);
}

const ocrResult = await this.databaseService.findOcrResult(documentId);

if (!ocrResult) {
this.logger.warn(`OCR result not found for document: ${documentId}`);
this.logger.warn(`Document status is: ${document.status}`);
throw new NotFoundException(
`OCR result not found for document: ${documentId}`,
`OCR result not found for document: ${documentId}. Current status: ${document.status}`,
);
}

this.logger.debug(
`OCR result retrieved successfully for document: ${documentId}`,
);
this.logger.debug(`OCR processed at: ${ocrResult.processed_at}`);
this.logger.debug("=== DocumentController.getOcrResult completed ===");

return ocrResult;
Expand All @@ -122,4 +140,61 @@ export class DocumentController {
);
}
}

@Get("documents/:documentId/download")
@HttpCode(HttpStatus.OK)
async downloadDocument(
@Param("documentId") documentId: string,
@Res() res: Response,
): Promise<void> {
this.logger.debug(`=== DocumentController.downloadDocument ===`);
this.logger.debug(`Document ID: ${documentId}`);

try {
// Find the document
const document = await this.databaseService.findDocument(documentId);
if (!document) {
throw new NotFoundException(`Document not found: ${documentId}`);
}

// Resolve stored relative path to absolute (we only store relative paths)
const filePath = join(process.cwd(), document.file_path);

// Read file
const fileBuffer = await readFile(filePath);

// Set appropriate headers
const fileName = document.original_filename || `document-${documentId}`;
const mimeType =
document.file_type === "pdf"
? "application/pdf"
: document.file_type === "image"
? "image/jpeg"
: "application/octet-stream";

res.setHeader("Content-Type", mimeType);
res.setHeader("Content-Disposition", `inline; filename="${fileName}"`);
res.setHeader("Content-Length", fileBuffer.length);

this.logger.debug(
`Serving file: ${filePath} (${fileBuffer.length} bytes)`,
);
this.logger.debug(
"=== DocumentController.downloadDocument completed ===",
);

res.send(fileBuffer);
} catch (error) {
this.logger.error(`Error downloading document: ${error.message}`);
this.logger.error(`Stack: ${error.stack}`);

if (error instanceof NotFoundException) {
throw error;
}

throw new NotFoundException(
error.message || `Failed to download document: ${documentId}`,
);
}
}
}
10 changes: 6 additions & 4 deletions apps/backend-services/src/document/document.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,16 @@ export class DocumentService {

// Generate unique filename and path
const fileName = this.generateFileName(originalFilename, fileType);
const filePath = join(this.storagePath, fileName);
const fullFilePath = join(this.storagePath, fileName);
// Store relative path in database (relative to storage root)
const relativePath = `storage/documents/${fileName}`;

// Ensure storage directory exists
await this.ensureStorageDirectory();

// Save file to filesystem
await writeFile(filePath, fileBuffer);
this.logger.debug(`File saved to: ${filePath}`);
await writeFile(fullFilePath, fileBuffer);
this.logger.debug(`File saved to: ${fullFilePath}`);

// Store metadata in database via API
const documentData: Omit<
Expand All @@ -114,7 +116,7 @@ export class DocumentService {
> = {
title,
original_filename: originalFilename,
file_path: filePath,
file_path: relativePath,
file_type: fileType,
file_size: fileSize,
metadata: (metadata || {}) as JsonValue,
Expand Down
13 changes: 13 additions & 0 deletions apps/backend-services/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { Logger, ValidationPipe } from "@nestjs/common";
import { NestFactory } from "@nestjs/core";
import { json, urlencoded } from "body-parser";
import { AppModule } from "./app.module";

const logger = new Logger("Bootstrap");

async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);

app.use(
json({
limit: process.env.BODY_LIMIT || "50mb",
}),
);
app.use(
urlencoded({
limit: process.env.BODY_LIMIT || "50mb",
extended: true,
}),
);

// Enable CORS
app.enableCors({
origin: process.env.FRONTEND_URL || "http://localhost:3000",
Expand Down
2 changes: 1 addition & 1 deletion apps/backend-services/src/ocr/azureTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface AnalysisResponse {
status: string;
createdDateTime: string;
lastUpdatedDateTime: string;
analyzeResult: AnalysisResult;
analyzeResult?: AnalysisResult; // Optional when status is "running"
}

export interface AnalysisResult {
Expand Down
46 changes: 37 additions & 9 deletions apps/backend-services/src/ocr/ocr.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export interface OcrRequestResponse {
export class OcrService {
private readonly logger = new Logger(OcrService.name);
private readonly azureModelId: string;
private readonly storagePath: string;
private readonly azureEndpoint: string;
private readonly azureApiKey: string;

Expand All @@ -41,9 +40,6 @@ export class OcrService {
this.logger.warn(azureConfigMessage);
throw Error(azureConfigMessage);
}
this.storagePath =
this.configService.get<string>("STORAGE_PATH") ||
join(process.cwd(), "storage", "documents");
}

/**
Expand All @@ -61,9 +57,9 @@ export class OcrService {
);
}
try {
// Read file from filesystem
// TODO: Where is this actually meant to come from? Suggest separating to file service.
const filePath = `${this.storagePath}/${document.file_path}`;
// Resolve stored relative path to absolute (we only store relative paths)
const filePath = join(process.cwd(), document.file_path);

const fileBuffer = await readFile(filePath);
if (fileBuffer == null) throw Error("File not found.");
this.logger.debug(`File size: ${fileBuffer.length} bytes`);
Expand Down Expand Up @@ -160,12 +156,44 @@ export class OcrService {
}

const analysisResponse: AnalysisResponse = azureResponse.data;
const anaysisResult = analysisResponse.analyzeResult;
this.logger.debug(`Azure response status: ${analysisResponse.status}`);
this.logger.debug(
`Azure response created: ${analysisResponse.createdDateTime}`,
);
this.logger.debug(
`Azure response updated: ${analysisResponse.lastUpdatedDateTime}`,
);

// Log the full response for debugging
// this.logger.debug(`Full Azure response: ${JSON.stringify(analysisResponse, null, 2)}`);

// If status is "running", processing is not complete yet
if (analysisResponse.status === "running") {
this.logger.debug(
`OCR processing still running for document ${documentId}, will retry later`,
);
return null; // Indicate processing not complete
}

const analysisResult = analysisResponse.analyzeResult;
if (!analysisResult) {
throw new Error(
`No analyzeResult in Azure response for document ${documentId} (status: ${analysisResponse.status})`,
);
}

this.logger.debug(
`Analysis result content length: ${analysisResult.content?.length || 0}`,
);
this.logger.debug(
`Analysis result pages: ${analysisResult.pages?.length || 0}`,
);

// Update OCR results table
this.databaseService.upsertOcrResult({
documentId,
analysisResponse,
});
return anaysisResult;
return analysisResult;
}
}
3 changes: 3 additions & 0 deletions apps/backend-services/src/queue/queue.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Module } from "@nestjs/common";
import { DatabaseModule } from "../database/database.module";
import { OcrModule } from "../ocr/ocr.module";
import { QueueService } from "./queue.service";

@Module({
providers: [QueueService],
exports: [QueueService],
imports: [OcrModule, DatabaseModule],
})
export class QueueModule {}
Loading