Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion apps/backend-services/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Controller,
Get,
HttpStatus,
Logger,
Post,
Query,
Res,
Expand All @@ -23,6 +24,7 @@ import { Public } from "./public.decorator";
*/
@Controller("auth")
export class AuthController {
private readonly logger = new Logger(AuthController.name);
constructor(private readonly authService: AuthService) {}

/**
Expand Down Expand Up @@ -96,7 +98,7 @@ export class AuthController {
const redirectUrl = this.authService.buildAuthResultRedirect(resultId);
return res.redirect(redirectUrl);
} catch (error) {
console.error("OAuth callback handling failed:", error);
this.logger.error("OAuth callback handling failed:", error);
const redirectUrl =
this.authService.buildErrorRedirect("callback_failed");
return res.redirect(redirectUrl);
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