@@ -8,6 +8,10 @@ import {
88 Logger ,
99 Get ,
1010 Res ,
11+ Req ,
12+ UnauthorizedException ,
13+ NotFoundException ,
14+ InternalServerErrorException ,
1115} from '@nestjs/common' ;
1216import { FileInterceptor } from '@nestjs/platform-express' ;
1317import {
@@ -16,12 +20,21 @@ import {
1620} from '../services/document-processor.service' ;
1721import { Express } from 'express' ;
1822import { Response } from 'express' ;
23+ import { ReportsService } from '../../reports/reports.service' ;
24+ import { RequestWithUser } from '../../auth/auth.middleware' ;
25+ import { Readable } from 'stream' ;
26+ import { S3Client , GetObjectCommand } from '@aws-sdk/client-s3' ;
27+ import { ConfigService } from '@nestjs/config' ;
1928
2029@Controller ( 'document-processor' )
2130export class DocumentProcessorController {
2231 private readonly logger = new Logger ( DocumentProcessorController . name ) ;
2332
24- constructor ( private readonly documentProcessorService : DocumentProcessorService ) { }
33+ constructor (
34+ private readonly documentProcessorService : DocumentProcessorService ,
35+ private readonly reportsService : ReportsService ,
36+ private readonly configService : ConfigService ,
37+ ) { }
2538
2639 @Post ( 'upload' )
2740 @UseInterceptors ( FileInterceptor ( 'file' ) )
@@ -35,11 +48,17 @@ export class DocumentProcessorController {
3548 }
3649
3750 // Validate file type
38- const validMimeTypes = [ 'image/jpeg' , 'image/png' , 'image/tiff' , 'application/pdf' ] ;
51+ const validMimeTypes = [
52+ 'image/jpeg' ,
53+ 'image/png' ,
54+ 'image/heic' ,
55+ 'image/heif' ,
56+ 'application/pdf' ,
57+ ] ;
3958
4059 if ( ! validMimeTypes . includes ( file . mimetype ) ) {
4160 throw new BadRequestException (
42- `Invalid file type: ${ file . mimetype } . Supported types: JPEG, PNG, TIFF , and PDF.` ,
61+ `Invalid file type: ${ file . mimetype } . Supported types: JPEG, PNG, HEIC, HEIF , and PDF.` ,
4362 ) ;
4463 }
4564
@@ -63,7 +82,6 @@ export class DocumentProcessorController {
6382 // Process the document
6483 const result = await this . documentProcessorService . processDocument (
6584 file . buffer ,
66- file . mimetype ,
6785 effectiveUserId ,
6886 ) ;
6987
@@ -95,6 +113,138 @@ export class DocumentProcessorController {
95113 }
96114 }
97115
116+ @Post ( 'process-file' )
117+ async processFileFromPath (
118+ @Body ( 'filePath' ) filePath : string ,
119+ @Req ( ) request : RequestWithUser ,
120+ ) : Promise < ProcessedDocumentResult | any > {
121+ if ( ! filePath ) {
122+ throw new BadRequestException ( 'No filePath provided' ) ;
123+ }
124+
125+ // Extract userId from the request (attached by auth middleware)
126+ const userId = request . user ?. sub ;
127+ if ( ! userId ) {
128+ throw new UnauthorizedException ( 'User ID not found in request' ) ;
129+ }
130+
131+ this . logger . log ( `Processing document from file path: ${ filePath } ` ) ;
132+
133+ try {
134+ // Fetch the associated report record from DynamoDB
135+ const report = await this . reportsService . findByFilePath ( filePath , userId ) ;
136+ if ( ! report ) {
137+ throw new NotFoundException ( `Report with filePath ${ filePath } not found` ) ;
138+ }
139+
140+ // Get the file from S3
141+ const fileBuffer = await this . getFileFromS3 ( filePath ) ;
142+
143+ // Process the document
144+ const result = await this . documentProcessorService . processDocument ( fileBuffer , userId ) ;
145+
146+ // Update the report with analysis results
147+ report . title = result . analysis . title || 'Untitled Report' ;
148+ report . category = result . analysis . category || 'general' ;
149+ report . isProcessed = true ;
150+
151+ // Extract lab values
152+ report . labValues = result . analysis . labValues || [ ] ;
153+
154+ // Create summary from simplified explanation or diagnoses
155+ report . summary =
156+ result . simplifiedExplanation ||
157+ result . analysis . diagnoses . map ( d => d . condition ) . join ( ', ' ) ||
158+ 'No summary available' ;
159+
160+ report . updatedAt = new Date ( ) . toISOString ( ) ;
161+
162+ // Update the report in DynamoDB
163+ await this . reportsService . updateReport ( report ) ;
164+
165+ return {
166+ success : true ,
167+ reportId : report . id ,
168+ analysis : result . analysis ,
169+ } ;
170+ } catch ( error : unknown ) {
171+ this . logger . error (
172+ `Error processing document from path ${ filePath } : ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
173+ ) ;
174+ throw error ;
175+ }
176+ }
177+
178+ /**
179+ * Retrieves a file from S3 storage
180+ * @param filePath - The S3 key of the file
181+ * @returns Buffer containing the file data
182+ */
183+ private async getFileFromS3 ( filePath : string ) : Promise < Buffer > {
184+ try {
185+ const bucketName = this . configService . get < string > ( 'aws.s3.uploadBucket' ) ;
186+ if ( ! bucketName ) {
187+ throw new InternalServerErrorException ( 'S3 bucket name not configured' ) ;
188+ }
189+
190+ const region = this . configService . get < string > ( 'aws.region' ) || 'us-east-1' ;
191+
192+ // Get optional AWS credentials if they exist
193+ const accessKeyId = this . configService . get < string > ( 'aws.aws.accessKeyId' ) ;
194+ const secretAccessKey = this . configService . get < string > ( 'aws.aws.secretAccessKey' ) ;
195+ const sessionToken = this . configService . get < string > ( 'aws.aws.sessionToken' ) ;
196+
197+ // Create S3 client with credentials if they exist
198+ const s3ClientOptions : any = { region } ;
199+
200+ if ( accessKeyId && secretAccessKey ) {
201+ s3ClientOptions . credentials = {
202+ accessKeyId,
203+ secretAccessKey,
204+ ...( sessionToken && { sessionToken } ) ,
205+ } ;
206+ }
207+
208+ const s3Client = new S3Client ( s3ClientOptions ) ;
209+
210+ const command = new GetObjectCommand ( {
211+ Bucket : bucketName ,
212+ Key : filePath ,
213+ } ) ;
214+
215+ const response = await s3Client . send ( command ) ;
216+
217+ // Check if response.Body exists before converting
218+ if ( ! response . Body ) {
219+ throw new InternalServerErrorException ( 'Empty response from S3' ) ;
220+ }
221+
222+ // Convert the readable stream to a buffer
223+ return await this . streamToBuffer ( response . Body as Readable ) ;
224+ } catch ( error ) {
225+ this . logger . error (
226+ `Error retrieving file from S3: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
227+ ) ;
228+ throw new InternalServerErrorException (
229+ `Failed to retrieve file from S3: ${ error instanceof Error ? error . message : 'Unknown error' } ` ,
230+ ) ;
231+ }
232+ }
233+
234+ /**
235+ * Converts a readable stream to a buffer
236+ * @param stream - The readable stream from S3
237+ * @returns Buffer containing the stream data
238+ */
239+ private async streamToBuffer ( stream : Readable ) : Promise < Buffer > {
240+ return new Promise ( ( resolve , reject ) => {
241+ const chunks : Buffer [ ] = [ ] ;
242+ stream . on ( 'data' , chunk => chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk ) ) ) ;
243+ stream . on ( 'end' , ( ) => resolve ( Buffer . concat ( chunks ) ) ) ;
244+ stream . on ( 'error' , reject ) ;
245+ } ) ;
246+ }
247+
98248 @Get ( 'test' )
99249 getTestStatus ( ) : { status : string } {
100250 return { status : 'DocumentProcessorController is working' } ;
@@ -229,8 +379,8 @@ export class DocumentProcessorController {
229379
230380 <form id="uploadForm" enctype="multipart/form-data">
231381 <div class="form-group">
232- <label for="file">Select File (PDF, JPEG, PNG, TIFF ):</label>
233- <input type="file" id="file" name="file" accept=".pdf,.jpg,.jpeg,.png,.tiff ">
382+ <label for="file">Select File (PDF, JPEG, PNG, HEIC, HEIF ):</label>
383+ <input type="file" id="file" name="file" accept=".pdf,.jpg,.jpeg,.png,.heic,.heif ">
234384 </div>
235385
236386 <div id="filePreview"></div>
0 commit comments