From 095306d2c79c43463eaaf75f62cc4fb65dfdbc66 Mon Sep 17 00:00:00 2001 From: Allan Sene Date: Fri, 20 Feb 2026 11:38:12 -0300 Subject: [PATCH] FEAT: adding autodrive apis --- deploy/helm-chart/templates/deployment.yaml | 6 + deploy/helm-chart/values.yaml | 3 + src/app.module.ts | 6 + src/authentication/permissions.enum.ts | 87 ++++ .../autodrive-assistant.config.ts | 10 + .../autodrive-assistant.controller.ts | 201 +++++++ .../autodrive-assistant.module.ts | 13 + .../autodrive-assistant.service.ts | 94 ++++ .../autodrive-core/autodrive-core.config.ts | 10 + .../autodrive-core.controller.ts | 286 ++++++++++ .../autodrive-core/autodrive-core.module.ts | 13 + .../autodrive-core/autodrive-core.service.ts | 165 ++++++ .../autodrive-extractor.config.ts | 10 + .../autodrive-extractor.controller.ts | 491 ++++++++++++++++++ .../autodrive-extractor.module.ts | 13 + .../autodrive-extractor.service.ts | 165 ++++++ 16 files changed, 1573 insertions(+) create mode 100644 src/modules/autodrive-assistant/autodrive-assistant.config.ts create mode 100644 src/modules/autodrive-assistant/autodrive-assistant.controller.ts create mode 100644 src/modules/autodrive-assistant/autodrive-assistant.module.ts create mode 100644 src/modules/autodrive-assistant/autodrive-assistant.service.ts create mode 100644 src/modules/autodrive-core/autodrive-core.config.ts create mode 100644 src/modules/autodrive-core/autodrive-core.controller.ts create mode 100644 src/modules/autodrive-core/autodrive-core.module.ts create mode 100644 src/modules/autodrive-core/autodrive-core.service.ts create mode 100644 src/modules/autodrive-extractor/autodrive-extractor.config.ts create mode 100644 src/modules/autodrive-extractor/autodrive-extractor.controller.ts create mode 100644 src/modules/autodrive-extractor/autodrive-extractor.module.ts create mode 100644 src/modules/autodrive-extractor/autodrive-extractor.service.ts diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml index 68b8e1d..f53cd95 100644 --- a/deploy/helm-chart/templates/deployment.yaml +++ b/deploy/helm-chart/templates/deployment.yaml @@ -113,6 +113,12 @@ spec: value: {{ .Values.maestro.platform_api_url }} - name: STORAGE_EXPLORER_API_URL value: {{ .Values.maestro.storage_explorer_api_url | quote }} + - name: AUTODRIVE_EXTRACTOR_API_URL + value: {{ .Values.maestro.autodrive_extractor_api_url | quote }} + - name: AUTODRIVE_CORE_API_URL + value: {{ .Values.maestro.autodrive_core_api_url | quote }} + - name: AUTODRIVE_ASSISTANT_API_URL + value: {{ .Values.maestro.autodrive_assistant_api_url | quote }} - name: JWT_PRIVATE_KEY valueFrom: secretKeyRef: diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml index 4e431f5..0ab7ba1 100644 --- a/deploy/helm-chart/values.yaml +++ b/deploy/helm-chart/values.yaml @@ -48,6 +48,9 @@ maestro: open_group_id: 401573bb-334f-44b2-b30e-88d4cea31ae9 platform_api_url: https://oz8v2zid1e.execute-api.us-east-1.amazonaws.com storage_explorer_api_url: "https://storage-explorer-{customer}.dadosfera.ai/api" + autodrive_extractor_api_url: "https://autodrive-extractor-api-{customer}.dadosfera.ai" + autodrive_core_api_url: "https://autodrive-api-{customer}.dadosfera.ai" + autodrive_assistant_api_url: "https://autodrive-assistant-api-{customer}.dadosfera.ai" dedicated_proxy: "" restricted_ip: "" redis_host: "aaapzppmlyamkocqwstpo7zvopczyyiyuy6xzm2g6c5k4mq3a66be4a-0.redis.sa-saopaulo-1.oci.oraclecloud.com" diff --git a/src/app.module.ts b/src/app.module.ts index 0865ad2..d5490f0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -35,6 +35,9 @@ import { ShareMetadataModule } from './modules/share-metadata/share-metadata.mod import { ApiKeyModule } from './modules/api-key/api-key.module'; import { PlatformApiModule } from './modules/platform-api/platform-api.module'; import { StorageExplorerModule } from './modules/storage-explorer/storage-explorer.module'; +import { AutodriveExtractorModule } from './modules/autodrive-extractor/autodrive-extractor.module'; +import { AutodriveCoreModule } from './modules/autodrive-core/autodrive-core.module'; +import { AutodriveAssistantModule } from './modules/autodrive-assistant/autodrive-assistant.module'; @Module({ providers: [ @@ -77,6 +80,9 @@ import { StorageExplorerModule } from './modules/storage-explorer/storage-explor NetworkPolicyModule, PlatformApiModule, StorageExplorerModule, + AutodriveExtractorModule, + AutodriveCoreModule, + AutodriveAssistantModule, //Always leave HealthModule last, so it is on the bottom of swagger HealthModule, ], diff --git a/src/authentication/permissions.enum.ts b/src/authentication/permissions.enum.ts index ca5e9ac..3038cf8 100644 --- a/src/authentication/permissions.enum.ts +++ b/src/authentication/permissions.enum.ts @@ -697,6 +697,93 @@ export const PERMISSIONS_GROUPS = { }, }, }, + AUTODRIVE_EXTRACTOR: { + title: { + 'pt-br': 'Autodrive Extractor', + 'en-us': 'Autodrive Extractor', + 'es-es': 'Autodrive Extractor', + }, + permissions: { + READ: { + seqid: 53, + claim: 'autodrive-extractor:read', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Ler dados do Autodrive Extractor', + 'en-us': 'Read Autodrive Extractor data', + 'es-es': 'Leer datos del Autodrive Extractor', + }, + }, + WRITE: { + seqid: 54, + claim: 'autodrive-extractor:write', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Escrever dados no Autodrive Extractor', + 'en-us': 'Write Autodrive Extractor data', + 'es-es': 'Escribir datos en Autodrive Extractor', + }, + }, + }, + }, + AUTODRIVE_CORE: { + title: { + 'pt-br': 'Autodrive Core', + 'en-us': 'Autodrive Core', + 'es-es': 'Autodrive Core', + }, + permissions: { + READ: { + seqid: 55, + claim: 'autodrive-core:read', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Ler dados do Autodrive Core', + 'en-us': 'Read Autodrive Core data', + 'es-es': 'Leer datos del Autodrive Core', + }, + }, + WRITE: { + seqid: 56, + claim: 'autodrive-core:write', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Escrever dados no Autodrive Core', + 'en-us': 'Write Autodrive Core data', + 'es-es': 'Escribir datos en Autodrive Core', + }, + }, + }, + }, + AUTODRIVE_ASSISTANT: { + title: { + 'pt-br': 'Autodrive Assistant', + 'en-us': 'Autodrive Assistant', + 'es-es': 'Autodrive Assistant', + }, + permissions: { + READ: { + seqid: 57, + claim: 'autodrive-assistant:read', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Ler dados do Autodrive Assistant', + 'en-us': 'Read Autodrive Assistant data', + 'es-es': 'Leer datos del Autodrive Assistant', + }, + }, + WRITE: { + seqid: 58, + claim: 'autodrive-assistant:write', + usage: PermissionUsages.PUBLIC, + name: { + 'pt-br': 'Escrever dados no Autodrive Assistant', + 'en-us': 'Write Autodrive Assistant data', + 'es-es': 'Escribir datos en Autodrive Assistant', + }, + }, + }, + }, }; export interface DadosferaModule { name: string; diff --git a/src/modules/autodrive-assistant/autodrive-assistant.config.ts b/src/modules/autodrive-assistant/autodrive-assistant.config.ts new file mode 100644 index 0000000..9406cad --- /dev/null +++ b/src/modules/autodrive-assistant/autodrive-assistant.config.ts @@ -0,0 +1,10 @@ +export const AUTODRIVE_ASSISTANT_CONFIG = { + getUrl: (customerName: string): string => { + const urlTemplate = process.env.AUTODRIVE_ASSISTANT_API_URL; + if (!urlTemplate) { + throw new Error('AUTODRIVE_ASSISTANT_API_URL environment variable is not set'); + } + return urlTemplate.replace('{customer}', customerName); + }, + timeout: parseInt(process.env.AUTODRIVE_ASSISTANT_TIMEOUT || '30000', 10), +}; diff --git a/src/modules/autodrive-assistant/autodrive-assistant.controller.ts b/src/modules/autodrive-assistant/autodrive-assistant.controller.ts new file mode 100644 index 0000000..bcae8df --- /dev/null +++ b/src/modules/autodrive-assistant/autodrive-assistant.controller.ts @@ -0,0 +1,201 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + Inject, +} from '@nestjs/common'; +import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { + Authenticated, + RequireAllPermissions, +} from '../../decorators/authentication.decorator'; +import { User, RequestUser } from '../../decorators/user.decorator'; +import { AutodriveAssistantService } from './autodrive-assistant.service'; +import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum'; + +@ApiTags('Autodrive Assistant') +@Controller('autodrive-assistant') +@Authenticated() +export class AutodriveAssistantController { + private logger: any; + + constructor( + private readonly autodriveAssistantService: AutodriveAssistantService, + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + // ============================================ + // ASSISTANTS + // ============================================ + + @ApiOperation({ summary: 'List assistants' }) + @Get('assistants') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.READ) + async listAssistants( + @Query('limit') limit: number, + @Query('offset') offset: number, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'GET', + '/assistants', + user, + undefined, + { limit, offset }, + ); + } + + @ApiOperation({ summary: 'Create assistant' }) + @Post('assistants') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.WRITE) + async createAssistant( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'POST', + '/assistants', + user, + body, + ); + } + + @ApiOperation({ summary: 'Get assistant by ID' }) + @Get('assistants/:assistantId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.READ) + async getAssistant( + @Param('assistantId') assistantId: string, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'GET', + `/assistants/${assistantId}`, + user, + ); + } + + @ApiOperation({ summary: 'Update assistant' }) + @Put('assistants/:assistantId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.WRITE) + async updateAssistant( + @Param('assistantId') assistantId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'PUT', + `/assistants/${assistantId}`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Delete assistant' }) + @Delete('assistants/:assistantId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.WRITE) + async deleteAssistant( + @Param('assistantId') assistantId: string, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'DELETE', + `/assistants/${assistantId}`, + user, + ); + } + + // ============================================ + // KNOWLEDGE BASE + // ============================================ + + @ApiOperation({ summary: 'List knowledge bases linked to assistant' }) + @Get('assistants/:assistantId/knowledge-bases') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.READ) + async listKnowledgeBases( + @Param('assistantId') assistantId: string, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'GET', + `/assistants/${assistantId}/knowledge-bases`, + user, + ); + } + + @ApiOperation({ summary: 'Update knowledge base associations for assistant' }) + @Put('assistants/:assistantId/knowledge-bases') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.WRITE) + async updateKnowledgeBases( + @Param('assistantId') assistantId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'PUT', + `/assistants/${assistantId}/knowledge-bases`, + user, + body, + ); + } + + // ============================================ + // QUESTIONS + // ============================================ + + @ApiOperation({ summary: 'Ask question via assistant' }) + @Post('dataset/:datasetId/ai_question') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.WRITE) + async aiQuestion( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'POST', + `/dataset/${datasetId}/ai_question`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Get AI question answer' }) + @Get('dataset/:datasetId/ai_question/:questionId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.READ) + async getAiQuestionResult( + @Param('datasetId') datasetId: string, + @Param('questionId') questionId: string, + @User() user: RequestUser, + ) { + return this.autodriveAssistantService.proxy( + 'GET', + `/dataset/${datasetId}/ai_question/${questionId}`, + user, + ); + } + + // ============================================ + // UTILITY + // ============================================ + + @ApiOperation({ summary: 'Health check' }) + @Get('health') + async health(@User() user: RequestUser) { + return this.autodriveAssistantService.proxy('GET', '/health', user); + } + + @ApiOperation({ summary: 'Check model availability' }) + @Get('model-availability') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_ASSISTANT.permissions.READ) + async modelAvailability(@User() user: RequestUser) { + return this.autodriveAssistantService.proxy('GET', '/model-availability', user); + } +} diff --git a/src/modules/autodrive-assistant/autodrive-assistant.module.ts b/src/modules/autodrive-assistant/autodrive-assistant.module.ts new file mode 100644 index 0000000..e955748 --- /dev/null +++ b/src/modules/autodrive-assistant/autodrive-assistant.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { AutodriveAssistantController } from './autodrive-assistant.controller'; +import { AutodriveAssistantService } from './autodrive-assistant.service'; + +@Module({ + imports: [], + controllers: [AutodriveAssistantController], + providers: [AutodriveAssistantService, DadosferaLogger], + exports: [AutodriveAssistantService], +}) +export class AutodriveAssistantModule {} diff --git a/src/modules/autodrive-assistant/autodrive-assistant.service.ts b/src/modules/autodrive-assistant/autodrive-assistant.service.ts new file mode 100644 index 0000000..23540fb --- /dev/null +++ b/src/modules/autodrive-assistant/autodrive-assistant.service.ts @@ -0,0 +1,94 @@ +import { Injectable, Inject, HttpException } from '@nestjs/common'; +import axios, { AxiosResponse, Method } from 'axios'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { RequestUser } from '../../decorators/user.decorator'; +import { AUTODRIVE_ASSISTANT_CONFIG } from './autodrive-assistant.config'; + +@Injectable() +export class AutodriveAssistantService { + private logger: any; + + constructor( + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + async proxy( + method: string, + path: string, + user: RequestUser, + body?: any, + query?: Record + ): Promise { + if (!user.customer_id) { + throw new HttpException('Customer ID is required for autodrive assistant operations', 400); + } + + const baseUrl = AUTODRIVE_ASSISTANT_CONFIG.getUrl(user.customer_name); + const url = new URL(`${baseUrl}${path}`); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + 'content-type': 'application/json', + 'Authorization': user.access_token, + }; + + this.logger.info('Proxying request to autodrive-assistant', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: body, + timeout: AUTODRIVE_ASSISTANT_CONFIG.timeout, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Autodrive Assistant API proxy error', { + error: error.message, + status: error.response?.status, + path, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + if (error.code === 'ECONNREFUSED') { + throw new HttpException('Autodrive Assistant API service unavailable', 503); + } + + if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') { + throw new HttpException('Autodrive Assistant API request timeout', 504); + } + + throw new HttpException('Internal server error', 500); + } + } +} diff --git a/src/modules/autodrive-core/autodrive-core.config.ts b/src/modules/autodrive-core/autodrive-core.config.ts new file mode 100644 index 0000000..ac3c5bd --- /dev/null +++ b/src/modules/autodrive-core/autodrive-core.config.ts @@ -0,0 +1,10 @@ +export const AUTODRIVE_CORE_CONFIG = { + getUrl: (customerName: string): string => { + const urlTemplate = process.env.AUTODRIVE_CORE_API_URL; + if (!urlTemplate) { + throw new Error('AUTODRIVE_CORE_API_URL environment variable is not set'); + } + return urlTemplate.replace('{customer}', customerName); + }, + timeout: parseInt(process.env.AUTODRIVE_CORE_TIMEOUT || '30000', 10), +}; diff --git a/src/modules/autodrive-core/autodrive-core.controller.ts b/src/modules/autodrive-core/autodrive-core.controller.ts new file mode 100644 index 0000000..0a660d1 --- /dev/null +++ b/src/modules/autodrive-core/autodrive-core.controller.ts @@ -0,0 +1,286 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + Inject, + UseInterceptors, + UploadedFiles, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; +import FormData from 'form-data'; + +import { + Authenticated, + RequireAllPermissions, +} from '../../decorators/authentication.decorator'; +import { User, RequestUser } from '../../decorators/user.decorator'; +import { AutodriveCoreService } from './autodrive-core.service'; +import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum'; + +@ApiTags('Autodrive Core') +@Controller('autodrive-core') +@Authenticated() +export class AutodriveCoreController { + private logger: any; + + constructor( + private readonly autodriveCoreService: AutodriveCoreService, + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + // ============================================ + // DATASET MANAGEMENT + // ============================================ + + @ApiOperation({ summary: 'List datasets' }) + @Get('datasets') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async listDatasets( + @Query('name') name: string, + @Query('limit') limit: number, + @Query('offset') offset: number, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'GET', + '/datasets', + user, + undefined, + { name, limit, offset }, + ); + } + + @ApiOperation({ summary: 'Get dataset by ID' }) + @Get('dataset/:datasetId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getDataset( + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'GET', + `/dataset/${datasetId}`, + user, + ); + } + + @ApiOperation({ summary: 'Add documents to existing dataset' }) + @Put('dataset/:datasetId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.WRITE) + async updateDataset( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'PUT', + `/dataset/${datasetId}`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Delete dataset' }) + @Delete('dataset/:datasetId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.WRITE) + async deleteDataset( + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'DELETE', + `/dataset/${datasetId}`, + user, + ); + } + + @ApiOperation({ summary: 'Append documents from URLs to dataset' }) + @Post('dataset/:datasetId/append-from-url') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.WRITE) + async appendFromUrl( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'POST', + `/dataset/${datasetId}/append-from-url`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Get dataset creation/update logs' }) + @Get('dataset/:datasetId/logs') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getDatasetLogs( + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'GET', + `/dataset/${datasetId}/logs`, + user, + ); + } + + // ============================================ + // FILE OPERATIONS + // ============================================ + + @ApiOperation({ summary: 'Create vector dataset from uploaded files' }) + @Post('upload') + @ApiConsumes('multipart/form-data') + @UseInterceptors(FilesInterceptor('files')) + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.WRITE) + async upload( + @UploadedFiles() files: Array, + @Body() body: any, + @User() user: RequestUser, + ) { + const formData = new FormData(); + if (files && files.length > 0) { + files.forEach((file) => { + formData.append('files', file.buffer, { + filename: file.originalname, + contentType: file.mimetype, + }); + }); + } + // Forward additional body fields + if (body) { + Object.entries(body).forEach(([key, value]) => { + if (value !== undefined && value !== null && key !== 'files') { + formData.append(key, String(value)); + } + }); + } + return this.autodriveCoreService.proxyFormData( + 'POST', + '/upload', + user, + formData, + ); + } + + @ApiOperation({ summary: 'Create vector dataset from URLs' }) + @Post('upload-from-url') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.WRITE) + async uploadFromUrl( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'POST', + '/upload-from-url', + user, + body, + ); + } + + // ============================================ + // QUERY OPERATIONS + // ============================================ + + @ApiOperation({ summary: 'Search dataset using semantic search' }) + @Post('dataset/:datasetId/question') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async question( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'POST', + `/dataset/${datasetId}/question`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Get semantic search results' }) + @Get('dataset/:datasetId/question/:questionId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getQuestionResult( + @Param('datasetId') datasetId: string, + @Param('questionId') questionId: string, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'GET', + `/dataset/${datasetId}/question/${questionId}`, + user, + ); + } + + @ApiOperation({ summary: 'Ask AI question on dataset' }) + @Post('dataset/:datasetId/ai_question') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async aiQuestion( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'POST', + `/dataset/${datasetId}/ai_question`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Get AI question answer' }) + @Get('dataset/:datasetId/ai_question/:questionId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getAiQuestionResult( + @Param('datasetId') datasetId: string, + @Param('questionId') questionId: string, + @User() user: RequestUser, + ) { + return this.autodriveCoreService.proxy( + 'GET', + `/dataset/${datasetId}/ai_question/${questionId}`, + user, + ); + } + + // ============================================ + // SYSTEM + // ============================================ + + @ApiOperation({ summary: 'Health check' }) + @Get('health') + async health(@User() user: RequestUser) { + return this.autodriveCoreService.proxy('GET', '/health', user); + } + + @ApiOperation({ summary: 'Check model availability' }) + @Get('model-availability') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async modelAvailability(@User() user: RequestUser) { + return this.autodriveCoreService.proxy('GET', '/model-availability', user); + } + + @ApiOperation({ summary: 'Get frontend configuration' }) + @Get('config') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getConfig(@User() user: RequestUser) { + return this.autodriveCoreService.proxy('GET', '/config', user); + } + + @ApiOperation({ summary: 'Get usage metrics' }) + @Get('usage') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_CORE.permissions.READ) + async getUsage(@User() user: RequestUser) { + return this.autodriveCoreService.proxy('GET', '/usage', user); + } +} diff --git a/src/modules/autodrive-core/autodrive-core.module.ts b/src/modules/autodrive-core/autodrive-core.module.ts new file mode 100644 index 0000000..84ea755 --- /dev/null +++ b/src/modules/autodrive-core/autodrive-core.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { AutodriveCoreController } from './autodrive-core.controller'; +import { AutodriveCoreService } from './autodrive-core.service'; + +@Module({ + imports: [], + controllers: [AutodriveCoreController], + providers: [AutodriveCoreService, DadosferaLogger], + exports: [AutodriveCoreService], +}) +export class AutodriveCoreModule {} diff --git a/src/modules/autodrive-core/autodrive-core.service.ts b/src/modules/autodrive-core/autodrive-core.service.ts new file mode 100644 index 0000000..d08c047 --- /dev/null +++ b/src/modules/autodrive-core/autodrive-core.service.ts @@ -0,0 +1,165 @@ +import { Injectable, Inject, HttpException } from '@nestjs/common'; +import axios, { AxiosResponse, Method } from 'axios'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { RequestUser } from '../../decorators/user.decorator'; +import { AUTODRIVE_CORE_CONFIG } from './autodrive-core.config'; + +@Injectable() +export class AutodriveCoreService { + private logger: any; + + constructor( + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + async proxy( + method: string, + path: string, + user: RequestUser, + body?: any, + query?: Record + ): Promise { + if (!user.customer_id) { + throw new HttpException('Customer ID is required for autodrive core operations', 400); + } + + const baseUrl = AUTODRIVE_CORE_CONFIG.getUrl(user.customer_name); + const url = new URL(`${baseUrl}${path}`); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + 'content-type': 'application/json', + 'Authorization': user.access_token, + }; + + this.logger.info('Proxying request to autodrive-core', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: body, + timeout: AUTODRIVE_CORE_CONFIG.timeout, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Autodrive Core API proxy error', { + error: error.message, + status: error.response?.status, + path, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + if (error.code === 'ECONNREFUSED') { + throw new HttpException('Autodrive Core API service unavailable', 503); + } + + if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') { + throw new HttpException('Autodrive Core API request timeout', 504); + } + + throw new HttpException('Internal server error', 500); + } + } + + async proxyFormData( + method: string, + path: string, + user: RequestUser, + formData: any, + query?: Record, + ): Promise { + if (!user.customer_id) { + throw new HttpException('Customer ID is required for autodrive core operations', 400); + } + + const baseUrl = AUTODRIVE_CORE_CONFIG.getUrl(user.customer_name); + const url = new URL(`${baseUrl}${path}`); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + ...formData.getHeaders?.(), + 'Authorization': user.access_token, + }; + + this.logger.info('Proxying form data request to autodrive-core', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: formData, + timeout: AUTODRIVE_CORE_CONFIG.timeout, + maxContentLength: Infinity, + maxBodyLength: Infinity, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Autodrive Core API form data proxy error', { + error: error.message, + status: error.response?.status, + path, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + throw new HttpException('Internal server error', 500); + } + } +} diff --git a/src/modules/autodrive-extractor/autodrive-extractor.config.ts b/src/modules/autodrive-extractor/autodrive-extractor.config.ts new file mode 100644 index 0000000..86882a4 --- /dev/null +++ b/src/modules/autodrive-extractor/autodrive-extractor.config.ts @@ -0,0 +1,10 @@ +export const AUTODRIVE_EXTRACTOR_CONFIG = { + getUrl: (customerName: string): string => { + const urlTemplate = process.env.AUTODRIVE_EXTRACTOR_API_URL; + if (!urlTemplate) { + throw new Error('AUTODRIVE_EXTRACTOR_API_URL environment variable is not set'); + } + return urlTemplate.replace('{customer}', customerName); + }, + timeout: parseInt(process.env.AUTODRIVE_EXTRACTOR_TIMEOUT || '30000', 10), +}; diff --git a/src/modules/autodrive-extractor/autodrive-extractor.controller.ts b/src/modules/autodrive-extractor/autodrive-extractor.controller.ts new file mode 100644 index 0000000..8f5a215 --- /dev/null +++ b/src/modules/autodrive-extractor/autodrive-extractor.controller.ts @@ -0,0 +1,491 @@ +import { + Controller, + Get, + Post, + Put, + Delete, + Param, + Body, + Query, + Inject, + UseInterceptors, + UploadedFiles, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger'; +import { FilesInterceptor } from '@nestjs/platform-express'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; +import FormData from 'form-data'; + +import { + Authenticated, + RequireAllPermissions, +} from '../../decorators/authentication.decorator'; +import { User, RequestUser } from '../../decorators/user.decorator'; +import { AutodriveExtractorService } from './autodrive-extractor.service'; +import { PERMISSIONS_GROUPS } from '../../authentication/permissions.enum'; + +@ApiTags('Autodrive Extractor') +@Controller('autodrive-extractor') +@Authenticated() +export class AutodriveExtractorController { + private logger: any; + + constructor( + private readonly autodriveExtractorService: AutodriveExtractorService, + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + // ============================================ + // LEGACY - NPL Extraction routes (to be removed) + // ============================================ + + @ApiOperation({ summary: '[LEGACY] Extract features from dataset using NPL' }) + @Post('npl/datasets/:datasetId/extract') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async nplExtract( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'POST', + `/npl/datasets/${datasetId}/extract`, + user, + body, + ); + } + + @ApiOperation({ summary: '[LEGACY] List NPL extractions for a dataset' }) + @Get('npl/datasets/:datasetId/extractions') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplListExtractions( + @Param('datasetId') datasetId: string, + @Query('status') status: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions`, + user, + undefined, + { status }, + ); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL extraction details' }) + @Get('npl/datasets/:datasetId/extractions/:extractionId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetExtraction( + @Param('datasetId') datasetId: string, + @Param('extractionId') extractionId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/${extractionId}`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Export NPL extraction to CSV' }) + @Get('npl/datasets/:datasetId/extractions/:extractionId/csv') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplExportExtractionCsv( + @Param('datasetId') datasetId: string, + @Param('extractionId') extractionId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/${extractionId}/csv`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Export NPL extraction to XLSX' }) + @Get('npl/datasets/:datasetId/extractions/:extractionId/xlsx') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplExportExtractionXlsx( + @Param('datasetId') datasetId: string, + @Param('extractionId') extractionId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/${extractionId}/xlsx`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Batch export all NPL extractions to CSV' }) + @Get('npl/datasets/:datasetId/extractions/export/csv') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplBatchExportCsv( + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/export/csv`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Batch export all NPL extractions to XLSX' }) + @Get('npl/datasets/:datasetId/extractions/export/xlsx') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplBatchExportXlsx( + @Param('datasetId') datasetId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/export/xlsx`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL extraction report' }) + @Get('npl/datasets/:datasetId/extractions/:extractionId/report') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetExtractionReport( + @Param('datasetId') datasetId: string, + @Param('extractionId') extractionId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/datasets/${datasetId}/extractions/${extractionId}/report`, + user, + ); + } + + // ============================================ + // LEGACY - NPL Configuration routes (to be removed) + // ============================================ + + @ApiOperation({ summary: '[LEGACY] Get NPL configuration' }) + @Get('npl/config') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetConfig(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config', user); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL configuration summary' }) + @Get('npl/config/summary') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetConfigSummary(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/summary', user); + } + + @ApiOperation({ summary: '[LEGACY] List NPL configuration fields' }) + @Get('npl/config/fields') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetConfigFields(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/fields', user); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL configuration field by ID' }) + @Get('npl/config/fields/:fieldId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetConfigField( + @Param('fieldId') fieldId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/npl/config/fields/${fieldId}`, + user, + ); + } + + @ApiOperation({ summary: '[LEGACY] Update NPL configuration field' }) + @Put('npl/config/fields/:fieldId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async nplUpdateConfigField( + @Param('fieldId') fieldId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'PUT', + `/npl/config/fields/${fieldId}`, + user, + body, + ); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL RAG queries' }) + @Get('npl/config/rag-queries') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetRagQueries(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/rag-queries', user); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL system prompt' }) + @Get('npl/config/system-prompt') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetSystemPrompt(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/system-prompt', user); + } + + @ApiOperation({ summary: '[LEGACY] Update NPL system prompt' }) + @Put('npl/config/system-prompt') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async nplUpdateSystemPrompt( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'PUT', + '/npl/config/system-prompt', + user, + body, + ); + } + + @ApiOperation({ summary: '[LEGACY] Get NPL source priorities' }) + @Get('npl/config/source-priorities') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplGetSourcePriorities(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/source-priorities', user); + } + + @ApiOperation({ summary: '[LEGACY] Reset NPL configuration to defaults' }) + @Post('npl/config/reset') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async nplResetConfig(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('POST', '/npl/config/reset', user); + } + + @ApiOperation({ summary: '[LEGACY] Export NPL configuration' }) + @Get('npl/config/export') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async nplExportConfig(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/npl/config/export', user); + } + + @ApiOperation({ summary: '[LEGACY] Import NPL configuration' }) + @Post('npl/config/import') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async nplImportConfig( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'POST', + '/npl/config/import', + user, + body, + ); + } + + // ============================================ + // EXTRACTION TEMPLATES + // ============================================ + + @ApiOperation({ summary: 'Seed NPL Brasil extraction template' }) + @Post('templates/seed-npl') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async seedNplTemplate( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'POST', + '/templates/seed-npl', + user, + body, + ); + } + + @ApiOperation({ summary: 'List extraction templates' }) + @Get('templates') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async listTemplates(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/templates', user); + } + + @ApiOperation({ summary: 'Create extraction template' }) + @Post('templates') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async createTemplate( + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'POST', + '/templates', + user, + body, + ); + } + + @ApiOperation({ summary: 'Get extraction template by ID' }) + @Get('templates/:templateId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async getTemplate( + @Param('templateId') templateId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/templates/${templateId}`, + user, + ); + } + + @ApiOperation({ summary: 'Update extraction template' }) + @Put('templates/:templateId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async updateTemplate( + @Param('templateId') templateId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'PUT', + `/templates/${templateId}`, + user, + body, + ); + } + + @ApiOperation({ summary: 'Delete extraction template' }) + @Delete('templates/:templateId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async deleteTemplate( + @Param('templateId') templateId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'DELETE', + `/templates/${templateId}`, + user, + ); + } + + @ApiOperation({ summary: 'Import extraction templates from file' }) + @Post('templates/import') + @ApiConsumes('multipart/form-data') + @UseInterceptors(FilesInterceptor('file')) + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async importTemplates( + @UploadedFiles() files: Array, + @User() user: RequestUser, + ) { + const formData = new FormData(); + if (files && files.length > 0) { + files.forEach((file) => { + formData.append('file', file.buffer, { + filename: file.originalname, + contentType: file.mimetype, + }); + }); + } + return this.autodriveExtractorService.proxyFormData( + 'POST', + '/templates/import', + user, + formData, + ); + } + + // ============================================ + // GENERIC EXTRACTION + // ============================================ + + @ApiOperation({ summary: 'Start template-based extraction on dataset' }) + @Post('extraction/datasets/:datasetId/extract') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.WRITE) + async startExtraction( + @Param('datasetId') datasetId: string, + @Body() body: any, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'POST', + `/extraction/datasets/${datasetId}/extract`, + user, + body, + ); + } + + @ApiOperation({ summary: 'List extraction jobs for a dataset' }) + @Get('extraction/datasets/:datasetId/jobs') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async listExtractionJobs( + @Param('datasetId') datasetId: string, + @Query('status') status: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/extraction/datasets/${datasetId}/jobs`, + user, + undefined, + { status }, + ); + } + + @ApiOperation({ summary: 'Get extraction job details' }) + @Get('extraction/jobs/:jobId') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async getExtractionJob( + @Param('jobId') jobId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/extraction/jobs/${jobId}`, + user, + ); + } + + @ApiOperation({ summary: 'Get extraction job report' }) + @Get('extraction/jobs/:jobId/report') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async getExtractionJobReport( + @Param('jobId') jobId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/extraction/jobs/${jobId}/report`, + user, + ); + } + + @ApiOperation({ summary: 'Export extraction job to CSV' }) + @Get('extraction/jobs/:jobId/csv') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async exportExtractionJobCsv( + @Param('jobId') jobId: string, + @User() user: RequestUser, + ) { + return this.autodriveExtractorService.proxy( + 'GET', + `/extraction/jobs/${jobId}/csv`, + user, + ); + } + + // ============================================ + // UTILITY + // ============================================ + + @ApiOperation({ summary: 'Health check' }) + @Get('health') + async health(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/health', user); + } + + @ApiOperation({ summary: 'Check model availability' }) + @Get('model-availability') + @RequireAllPermissions(PERMISSIONS_GROUPS.AUTODRIVE_EXTRACTOR.permissions.READ) + async modelAvailability(@User() user: RequestUser) { + return this.autodriveExtractorService.proxy('GET', '/model-availability', user); + } +} diff --git a/src/modules/autodrive-extractor/autodrive-extractor.module.ts b/src/modules/autodrive-extractor/autodrive-extractor.module.ts new file mode 100644 index 0000000..de63736 --- /dev/null +++ b/src/modules/autodrive-extractor/autodrive-extractor.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { AutodriveExtractorController } from './autodrive-extractor.controller'; +import { AutodriveExtractorService } from './autodrive-extractor.service'; + +@Module({ + imports: [], + controllers: [AutodriveExtractorController], + providers: [AutodriveExtractorService, DadosferaLogger], + exports: [AutodriveExtractorService], +}) +export class AutodriveExtractorModule {} diff --git a/src/modules/autodrive-extractor/autodrive-extractor.service.ts b/src/modules/autodrive-extractor/autodrive-extractor.service.ts new file mode 100644 index 0000000..17b8ad3 --- /dev/null +++ b/src/modules/autodrive-extractor/autodrive-extractor.service.ts @@ -0,0 +1,165 @@ +import { Injectable, Inject, HttpException } from '@nestjs/common'; +import axios, { AxiosResponse, Method } from 'axios'; +import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; + +import { RequestUser } from '../../decorators/user.decorator'; +import { AUTODRIVE_EXTRACTOR_CONFIG } from './autodrive-extractor.config'; + +@Injectable() +export class AutodriveExtractorService { + private logger: any; + + constructor( + @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, + ) { + this.logger = dadosferaLogger.logger; + } + + async proxy( + method: string, + path: string, + user: RequestUser, + body?: any, + query?: Record + ): Promise { + if (!user.customer_id) { + throw new HttpException('Customer ID is required for autodrive extractor operations', 400); + } + + const baseUrl = AUTODRIVE_EXTRACTOR_CONFIG.getUrl(user.customer_name); + const url = new URL(`${baseUrl}${path}`); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + 'content-type': 'application/json', + 'Authorization': user.access_token, + }; + + this.logger.info('Proxying request to autodrive-extractor', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: body, + timeout: AUTODRIVE_EXTRACTOR_CONFIG.timeout, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Autodrive Extractor API proxy error', { + error: error.message, + status: error.response?.status, + path, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + if (error.code === 'ECONNREFUSED') { + throw new HttpException('Autodrive Extractor API service unavailable', 503); + } + + if (error.code === 'ETIMEDOUT' || error.code === 'ECONNABORTED') { + throw new HttpException('Autodrive Extractor API request timeout', 504); + } + + throw new HttpException('Internal server error', 500); + } + } + + async proxyFormData( + method: string, + path: string, + user: RequestUser, + formData: any, + query?: Record, + ): Promise { + if (!user.customer_id) { + throw new HttpException('Customer ID is required for autodrive extractor operations', 400); + } + + const baseUrl = AUTODRIVE_EXTRACTOR_CONFIG.getUrl(user.customer_name); + const url = new URL(`${baseUrl}${path}`); + + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + }); + } + + const headers: Record = { + ...formData.getHeaders?.(), + 'Authorization': user.access_token, + }; + + this.logger.info('Proxying form data request to autodrive-extractor', { + method: method.toUpperCase(), + path, + customer_id: user.customer_id, + user_id: user.user_id, + }); + + try { + const response: AxiosResponse = await axios({ + method: method as Method, + url: url.href, + headers, + data: formData, + timeout: AUTODRIVE_EXTRACTOR_CONFIG.timeout, + maxContentLength: Infinity, + maxBodyLength: Infinity, + validateStatus: () => true, + }); + + if (response.status >= 400) { + throw new HttpException(response.data, response.status); + } + + return response.data; + } catch (error) { + this.logger.error('Autodrive Extractor API form data proxy error', { + error: error.message, + status: error.response?.status, + path, + method: method.toUpperCase(), + }); + + if (error instanceof HttpException) { + throw error; + } + + if (error.response) { + throw new HttpException(error.response.data, error.response.status); + } + + throw new HttpException('Internal server error', 500); + } + } +}