diff --git a/apps/api/package-lock.json b/apps/api/package-lock.json index 941dca61..6fe63900 100644 --- a/apps/api/package-lock.json +++ b/apps/api/package-lock.json @@ -227,6 +227,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2839,6 +2840,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3009,6 +3011,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.2", "iterare": "1.2.1", @@ -3041,6 +3044,7 @@ "integrity": "sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3114,6 +3118,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3527,6 +3532,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3650,6 +3656,7 @@ "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3788,6 +3795,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -4219,6 +4227,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4268,6 +4277,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4627,6 +4637,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4843,6 +4854,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -4875,6 +4887,7 @@ "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -5650,6 +5663,7 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5710,6 +5724,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10624,6 +10639,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10827,7 +10843,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/require-directory": { "version": "2.1.1", @@ -11616,6 +11633,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11957,6 +11975,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12117,6 +12136,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -12311,6 +12331,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12635,7 +12656,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12654,7 +12674,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12668,7 +12687,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12683,7 +12701,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -12693,8 +12710,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", @@ -12702,7 +12718,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -12713,7 +12728,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12727,7 +12741,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/apps/api/src/Analytics-Export/analytics-record.interface.ts b/apps/api/src/Analytics-Export/analytics-record.interface.ts index 6835648b..4d8c8086 100644 --- a/apps/api/src/Analytics-Export/analytics-record.interface.ts +++ b/apps/api/src/Analytics-Export/analytics-record.interface.ts @@ -1,4 +1,4 @@ -import { AnalyticsMetric } from '../enums/analytics-metric.enum'; +import { AnalyticsMetric } from './analytics-metric.enum'; export interface AnalyticsRecord { id: string; diff --git a/apps/api/src/Analytics-Export/csv-builder.util.ts b/apps/api/src/Analytics-Export/csv-builder.util.ts index abb8fe4c..d818fdfd 100644 --- a/apps/api/src/Analytics-Export/csv-builder.util.ts +++ b/apps/api/src/Analytics-Export/csv-builder.util.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { CsvBuildOptions, CsvColumn } from '../interfaces/export-options.interface'; -import { AnalyticsRecord } from '../interfaces/analytics-record.interface'; +import { CsvBuildOptions, CsvColumn } from './export-options.interface'; +import { AnalyticsRecord } from './analytics-record.interface'; @Injectable() export class CsvBuilderUtil { @@ -9,10 +9,7 @@ export class CsvBuilderUtil { /** * Build a complete CSV string from an array of records. */ - build( - records: AnalyticsRecord[], - options: CsvBuildOptions, - ): string { + build(records: AnalyticsRecord[], options: CsvBuildOptions): string { const lines: string[] = []; if (options.includeHeader) { @@ -46,7 +43,9 @@ export class CsvBuilderUtil { * Build the header row. */ buildHeader(columns: CsvColumn[], delimiter: string): string { - return columns.map((col) => this.escapeField(col.header, delimiter)).join(delimiter); + return columns + .map((col) => this.escapeField(col.header, delimiter)) + .join(delimiter); } /** @@ -57,7 +56,10 @@ export class CsvBuilderUtil { return columns .map((col) => { - const rawValue = this.getNestedValue(record, col.key); + const rawValue = this.getNestedValue( + record as unknown as Record, + col.key, + ); let formatted: string; if (rawValue === null || rawValue === undefined) { diff --git a/apps/api/src/Analytics-Export/export-options.interface.ts b/apps/api/src/Analytics-Export/export-options.interface.ts index ed4581cd..9c265ba6 100644 --- a/apps/api/src/Analytics-Export/export-options.interface.ts +++ b/apps/api/src/Analytics-Export/export-options.interface.ts @@ -1,4 +1,4 @@ -import { AnalyticsMetric } from '../enums/analytics-metric.enum'; +import { AnalyticsMetric } from './analytics-metric.enum'; export interface ExportOptions { metrics: AnalyticsMetric[]; diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 02a62599..681810bf 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -26,6 +26,7 @@ import { IntelligenceHubModule } from './intelligence-hub/stellar/intelligence-h import { AssetDiscoveryModule } from './api/assets/discovery/stellar/asset-discovery.module'; import { RecommendationMetricsModule } from './metrics/recommendations/recommendation-metrics.module'; import { StellarEcosystemMetricsModule } from './metrics/ecosystem/stellar/stellar-ecosystem-metrics.module'; +import { RouteInsightsExporterModule } from './exporters/routes/stellar/route-insights-exporter.module'; @Module({ imports: [ @@ -55,7 +56,7 @@ import { StellarEcosystemMetricsModule } from './metrics/ecosystem/stellar/stell AnalyticsModule, TokenMetadataModule, VersionModule, - StellarReputationModule, + StellarReputationModule, WalletModule, SorobanContractModule, StellarTimeoutModule, @@ -73,6 +74,7 @@ import { StellarEcosystemMetricsModule } from './metrics/ecosystem/stellar/stell // Explainability API for Stellar route recommendations // Exposed through /explainability/stellar endpoints. StellarExplainabilityModule, + RouteInsightsExporterModule, ], controllers: [AppController], providers: [ diff --git a/apps/api/src/exporters/routes/stellar/dto/route-insights-export.dto.ts b/apps/api/src/exporters/routes/stellar/dto/route-insights-export.dto.ts new file mode 100644 index 00000000..b43b70f7 --- /dev/null +++ b/apps/api/src/exporters/routes/stellar/dto/route-insights-export.dto.ts @@ -0,0 +1,197 @@ +import { + IsEnum, + IsOptional, + IsDateString, + IsIn, + IsBoolean, + IsString, +} from 'class-validator'; +import { Transform } from 'class-transformer'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; + +/** + * Route Insights Export Format Options + */ +export enum ExportFormat { + CSV = 'csv', + JSON = 'json', +} + +/** + * Route Insights Export Request DTO + */ +export class RouteInsightsExportDto { + @ApiProperty({ + description: 'Export format', + enum: ExportFormat, + default: ExportFormat.CSV, + }) + @IsEnum(ExportFormat) + @IsOptional() + format?: ExportFormat = ExportFormat.CSV; + + @ApiPropertyOptional({ + description: 'Start date for analytics range (ISO 8601)', + }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ + description: 'End date for analytics range (ISO 8601)', + }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiPropertyOptional({ + description: 'Filter by source chain', + }) + @IsOptional() + @IsString() + sourceChain?: string; + + @ApiPropertyOptional({ + description: 'Filter by destination chain', + }) + @IsOptional() + @IsString() + destinationChain?: string; + + @ApiPropertyOptional({ + description: 'Filter by bridge name', + }) + @IsOptional() + @IsString() + bridgeName?: string; + + @ApiPropertyOptional({ + description: 'Filter by token', + }) + @IsOptional() + @IsString() + token?: string; + + @ApiPropertyOptional({ + description: 'Include recommendation insights', + default: true, + }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + includeRecommendations?: boolean = true; + + @ApiPropertyOptional({ + description: 'Include performance metrics', + default: true, + }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + includeMetrics?: boolean = true; + + @ApiPropertyOptional({ + description: 'CSV delimiter (only for CSV format)', + default: ',', + }) + @IsOptional() + @IsIn([',', ';', '\t']) + delimiter?: ',' | ';' | '\t' = ','; + + @ApiPropertyOptional({ + description: 'Include route ranking', + default: true, + }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + includeRanking?: boolean = true; + + @ApiPropertyOptional({ + description: 'Async export for large datasets', + default: false, + }) + @IsOptional() + @IsBoolean() + @Transform(({ value }) => value === 'true' || value === true) + async?: boolean = false; +} + +/** + * Route Insights Data Point + */ +export class RouteInsightDataDto { + @ApiProperty() + bridgeName: string; + + @ApiProperty() + sourceChain: string; + + @ApiProperty() + destinationChain: string; + + @ApiPropertyOptional() + token?: string; + + @ApiProperty({ description: 'Total number of transfers' }) + totalTransfers: number; + + @ApiProperty({ description: 'Successful transfers' }) + successfulTransfers: number; + + @ApiProperty({ description: 'Failed transfers' }) + failedTransfers: number; + + @ApiProperty({ description: 'Success rate percentage' }) + successRate: number; + + @ApiPropertyOptional({ description: 'Average settlement time in ms' }) + averageSettlementTimeMs?: number; + + @ApiPropertyOptional({ description: 'Average fee' }) + averageFee?: number; + + @ApiPropertyOptional({ description: 'Average slippage percentage' }) + averageSlippagePercent?: number; + + @ApiProperty({ description: 'Total volume transferred' }) + totalVolume: number; + + @ApiPropertyOptional({ description: 'Recommendation rank (1 = best)' }) + recommendationRank?: number; + + @ApiPropertyOptional({ description: 'Recommendation score (0-500)' }) + recommendationScore?: number; + + @ApiProperty({ description: 'Last updated timestamp' }) + lastUpdated: Date; +} + +/** + * Route Insights Export Response + */ +export class RouteInsightsExportResponseDto { + @ApiProperty() + exportId: string; + + @ApiProperty() + format: ExportFormat; + + @ApiProperty() + rowCount: number; + + @ApiPropertyOptional() + downloadUrl?: string; + + @ApiProperty() + generatedAt: Date; + + @ApiPropertyOptional() + data?: RouteInsightDataDto[]; + + @ApiPropertyOptional({ description: 'For async exports' }) + statusUrl?: string; + + @ApiPropertyOptional({ description: 'For async exports' }) + status?: string; +} diff --git a/apps/api/src/exporters/routes/stellar/route-insights-exporter.controller.ts b/apps/api/src/exporters/routes/stellar/route-insights-exporter.controller.ts new file mode 100644 index 00000000..10ffb187 --- /dev/null +++ b/apps/api/src/exporters/routes/stellar/route-insights-exporter.controller.ts @@ -0,0 +1,153 @@ +import { + Controller, + Get, + Post, + Query, + Res, + Req, + UseGuards, + HttpCode, + HttpStatus, + Param, + NotFoundException, +} from '@nestjs/common'; +import { Response, Request } from 'express'; +import type { Response as ExpressResponse, Request as ExpressRequest } from 'express'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { RouteInsightsExporterService } from './route-insights-exporter.service'; +import { + RouteInsightsExportDto, + RouteInsightsExportResponseDto, + ExportFormat, +} from './dto/route-insights-export.dto'; +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; + +/** + * Placeholder JWT guard — swap for your actual AuthGuard + */ +@Injectable() +class AuthGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + return !!(req as Request & { user?: unknown }).user; + } +} + +@ApiTags('Exporters') +@Controller('exporters/routes/stellar') +@UseGuards(AuthGuard) +export class RouteInsightsExporterController { + constructor(private readonly exporterService: RouteInsightsExporterService) {} + + /** + * POST /exporters/routes/stellar/export + * + * Export Stellar route insights with analytics and recommendations. + * Supports CSV and JSON formats. + * + * Returns: + * - Sync: Immediate file download + * - Async: Job reference with polling URL + */ + @Post('export') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Export Stellar route insights', + description: + 'Export route analytics and recommendation insights for Stellar bridges. ' + + 'Supports CSV and JSON formats. Use async=true for large datasets.', + }) + @ApiResponse({ + status: 200, + description: 'Export completed successfully', + schema: { + example: { + exportId: 'uuid', + format: 'csv', + rowCount: 42, + downloadUrl: '/api/exporters/routes/stellar/download/uuid', + generatedAt: '2024-01-15T10:30:00Z', + }, + }, + }) + @ApiResponse({ status: 400, description: 'Invalid export parameters' }) + @ApiResponse({ + status: 404, + description: 'No routes found matching criteria', + }) + async exportRouteInsights( + @Query() dto: RouteInsightsExportDto, + @Req() req: ExpressRequest & { user: { id: string } }, + @Res() res: ExpressResponse, + ): Promise { + const userId = req.user.id; + const result = await this.exporterService.exportRouteInsights(userId, dto); + + // For now, all exports are sync and return JSON metadata + file info + if (dto.format === ExportFormat.JSON) { + res.setHeader('Content-Type', 'application/json'); + res.json(result); + } else { + // CSV is returned as downloadable file + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader( + 'Content-Disposition', + 'attachment; filename="stellar-route-insights.csv"', + ); + res.json(result); + } + } + + /** + * GET /exporters/routes/stellar/download/:exportId + * + * Download previously generated export + */ + @Get('download/:exportId') + @ApiOperation({ + summary: 'Download route insights export', + }) + @ApiParam({ name: 'exportId', description: 'Export ID from export response' }) + @ApiResponse({ + status: 200, + description: 'Export file download', + }) + @ApiResponse({ status: 404, description: 'Export not found' }) + async downloadExport( + @Param('exportId') exportId: string, + @Req() req: ExpressRequest & { user: { id: string } }, + @Res() res: ExpressResponse, + ): Promise { + try { + // Placeholder for actual download implementation + // In production, fetch from storage/cache and stream file + throw new NotFoundException(`Export ${exportId} not found or expired`); + } catch (error) { + res.status(HttpStatus.NOT_FOUND).json({ + statusCode: 404, + message: 'Export not found or has expired', + error: 'NotFound', + }); + } + } + + /** + * GET /exporters/routes/stellar/exports + * + * List recent route insights exports for authenticated user + */ + @Get('exports') + @ApiOperation({ + summary: 'List user route insights exports', + }) + @ApiResponse({ + status: 200, + description: 'List of recent exports', + type: [RouteInsightsExportResponseDto], + }) + async listExports( + @Req() req: ExpressRequest & { user: { id: string } }, + ): Promise { + return this.exporterService.listUserExports(req.user.id); + } +} diff --git a/apps/api/src/exporters/routes/stellar/route-insights-exporter.module.ts b/apps/api/src/exporters/routes/stellar/route-insights-exporter.module.ts new file mode 100644 index 00000000..7b73f51d --- /dev/null +++ b/apps/api/src/exporters/routes/stellar/route-insights-exporter.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RouteInsightsExporterService } from './route-insights-exporter.service'; +import { RouteInsightsExporterController } from './route-insights-exporter.controller'; +import { BridgeAnalytics } from '../../../analytics/entities/bridge-analytics.entity'; +import { AnalyticsModule } from '../../../analytics/analytics.module'; +import { CsvBuilderUtil } from '../../../Analytics-Export/csv-builder.util'; + +/** + * Route Insights Exporter Module + * + * Provides functionality to export Stellar route analytics and recommendation insights. + * Integrates with the Analytics module to fetch route metrics and applies recommendation + * scoring to provide actionable insights for external consumption. + * + * Supports: + * - CSV and JSON export formats + * - Route metrics (transfers, fees, slippage, settlement times) + * - Recommendation rankings and scoring + * - Date range filtering + * - Bridge/chain/token filtering + */ +@Module({ + imports: [TypeOrmModule.forFeature([BridgeAnalytics]), AnalyticsModule], + controllers: [RouteInsightsExporterController], + providers: [RouteInsightsExporterService, CsvBuilderUtil], + exports: [RouteInsightsExporterService], +}) +export class RouteInsightsExporterModule {} diff --git a/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.spec.ts b/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.spec.ts new file mode 100644 index 00000000..9a0c6119 --- /dev/null +++ b/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.spec.ts @@ -0,0 +1,137 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { RouteInsightsExporterService } from './route-insights-exporter.service'; +import { StellarAnalyticsService } from '../../../analytics/stellar/stellar-analytics.service'; +import { BridgeAnalytics } from '../../../analytics/entities/bridge-analytics.entity'; +import { CsvBuilderUtil } from '../../../Analytics-Export/csv-builder.util'; +import { RouteInsightsExportDto, ExportFormat } from './dto/route-insights-export.dto'; +import { NotFoundException } from '@nestjs/common'; + +describe('RouteInsightsExporterService', () => { + let service: RouteInsightsExporterService; + let mockAnalyticsRepo: any; + let mockStellarAnalyticsService: any; + let mockCsvBuilder: any; + + beforeEach(async () => { + // Mock dependencies + mockAnalyticsRepo = {}; + + mockStellarAnalyticsService = { + getStellarAnalytics: jest.fn(), + }; + + mockCsvBuilder = { + build: jest.fn(), + estimateSize: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RouteInsightsExporterService, + { + provide: getRepositoryToken(BridgeAnalytics), + useValue: mockAnalyticsRepo, + }, + { + provide: StellarAnalyticsService, + useValue: mockStellarAnalyticsService, + }, + { + provide: CsvBuilderUtil, + useValue: mockCsvBuilder, + }, + ], + }).compile(); + + service = module.get(RouteInsightsExporterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should throw NotFoundException when no routes found', async () => { + const dto: RouteInsightsExportDto = { + format: ExportFormat.CSV, + }; + + mockStellarAnalyticsService.getStellarAnalytics.mockResolvedValue({ + data: [], + total: 0, + }); + + await expect(service.exportRouteInsights('user-123', dto)).rejects.toThrow( + NotFoundException, + ); + }); + + it('should throw BadRequestException for large sync exports', async () => { + const dto: RouteInsightsExportDto = { + format: ExportFormat.CSV, + async: false, + }; + + // Create array of routes larger than SYNC_ROW_LIMIT (5000) + const largeRoutes = Array.from({ length: 5001 }, (_, i) => ({ + bridgeName: `bridge-${i}`, + sourceChain: 'Stellar', + destinationChain: 'Ethereum', + totalTransfers: 100, + successfulTransfers: 95, + failedTransfers: 5, + successRate: 95, + totalVolume: 1000, + lastUpdated: new Date(), + })); + + mockStellarAnalyticsService.getStellarAnalytics.mockResolvedValue({ + data: largeRoutes, + total: largeRoutes.length, + }); + + await expect(service.exportRouteInsights('user-123', dto)).rejects.toThrow(); + }); + + it('should successfully export route insights', async () => { + const dto: RouteInsightsExportDto = { + format: ExportFormat.CSV, + includeRecommendations: true, + includeRanking: true, + }; + + const mockRoutes = [ + { + bridgeName: 'Stellar-Bridge', + sourceChain: 'Stellar', + destinationChain: 'Ethereum', + token: 'USDC', + totalTransfers: 100, + successfulTransfers: 95, + failedTransfers: 5, + successRate: 95, + averageSettlementTimeMs: 30000, + averageFee: 0.5, + averageSlippagePercent: 0.1, + totalVolume: 100000, + lastUpdated: new Date(), + }, + ]; + + mockStellarAnalyticsService.getStellarAnalytics.mockResolvedValue({ + data: mockRoutes, + total: 1, + }); + + mockCsvBuilder.build.mockReturnValue('csv,data'); + mockCsvBuilder.estimateSize.mockReturnValue(8); + + const result = await service.exportRouteInsights('user-123', dto); + + expect(result).toHaveProperty('exportId'); + expect(result).toHaveProperty('format', ExportFormat.CSV); + expect(result).toHaveProperty('rowCount', 1); + expect(result).toHaveProperty('downloadUrl'); + expect(result).toHaveProperty('generatedAt'); + }); +}); diff --git a/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.ts b/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.ts new file mode 100644 index 00000000..25778170 --- /dev/null +++ b/apps/api/src/exporters/routes/stellar/route-insights-exporter.service.ts @@ -0,0 +1,323 @@ +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, Between, In, FindOptionsWhere } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; +import { BridgeAnalytics } from '../../../analytics/entities/bridge-analytics.entity'; +import { StellarAnalyticsService } from '../../../analytics/stellar/stellar-analytics.service'; +import { + BridgeRoute, + recommendBridgeRoutes, +} from '../../../bridge-recommendation/bridge-recommendation.engine'; +import { + RouteInsightsExportDto, + ExportFormat, + RouteInsightDataDto, + RouteInsightsExportResponseDto, +} from './dto/route-insights-export.dto'; +import { CsvBuilderUtil } from '../../../Analytics-Export/csv-builder.util'; +import { getAllChains } from '../../../config/chains.config'; +import { BridgeAnalyticsQueryDto } from '../../../analytics/dto/bridge-analytics.dto'; + +const SYNC_ROW_LIMIT = 5_000; + +/** + * Stellar Route Insights Exporter Service + * + * Handles exporting route analytics and recommendation insights for Stellar bridges. + * Supports multiple export formats (CSV, JSON) with async processing for large datasets. + */ +@Injectable() +export class RouteInsightsExporterService { + private readonly logger = new Logger(RouteInsightsExporterService.name); + + constructor( + @InjectRepository(BridgeAnalytics) + private readonly analyticsRepository: Repository, + private readonly stellarAnalyticsService: StellarAnalyticsService, + private readonly csvBuilder: CsvBuilderUtil, + ) {} + + /** + * Initiate route insights export + */ + async exportRouteInsights( + userId: string, + dto: RouteInsightsExportDto, + ): Promise { + const exportId = uuidv4(); + + // Build query for Stellar routes + const query = this.buildAnalyticsQuery(dto); + + // Fetch route analytics + const analyticsResponse = + await this.stellarAnalyticsService.getStellarAnalytics(query); + const routes = analyticsResponse.data; + + if (routes.length === 0) { + throw new NotFoundException( + 'No Stellar routes found matching the criteria', + ); + } + + // Check size limits + if (routes.length > SYNC_ROW_LIMIT && !dto.async) { + throw new BadRequestException( + `Dataset too large (${routes.length} routes). Use async=true for large exports.`, + ); + } + + // Build insights data + const insightsData = await this.buildInsightsData( + routes, + dto.includeRecommendations ?? true, + dto.includeRanking ?? true, + ); + + // Generate export based on format + if (dto.format === ExportFormat.JSON) { + return this.generateJsonExport(exportId, insightsData, userId); + } else { + return this.generateCsvExport( + exportId, + insightsData, + dto.delimiter ?? ',', + userId, + ); + } + } + + /** + * Build route insights data with recommendations and metrics + */ + private async buildInsightsData( + routes: any[], + includeRecommendations: boolean, + includeRanking: boolean, + ): Promise { + const insightsData: RouteInsightDataDto[] = []; + + // Convert routes to bridge routes for recommendation scoring + const bridgeRoutes: BridgeRoute[] = routes.map((route) => ({ + bridgeName: route.bridgeName, + sourceChain: route.sourceChain, + destinationChain: route.destinationChain, + token: route.token || '', + fee: route.averageFee ? Number(route.averageFee) : 0, + slippage: route.averageSlippagePercent + ? Number(route.averageSlippagePercent) + : 0, + estimatedTime: route.averageSettlementTimeMs + ? Number(route.averageSettlementTimeMs) + : 0, + reliabilityScore: route.successRate / 100, + historicalSuccessRate: route.successRate / 100, + })); + + // Get recommendations if requested + let recommendations: any[] = []; + if (includeRecommendations && bridgeRoutes.length > 0) { + const recommendationResult = recommendBridgeRoutes({ + sourceChain: bridgeRoutes[0].sourceChain, + destinationChain: bridgeRoutes[0].destinationChain, + token: bridgeRoutes[0].token, + amount: 1, + account: 'insights-export', + routes: bridgeRoutes, + }); + recommendations = recommendationResult.rankedRoutes; + } + + // Build insights combining analytics and recommendations + for (let i = 0; i < routes.length; i++) { + const route = routes[i]; + const recommendation = recommendations.find( + (r) => r.route.bridgeName === route.bridgeName, + ); + + const insight: RouteInsightDataDto = { + bridgeName: route.bridgeName, + sourceChain: route.sourceChain, + destinationChain: route.destinationChain, + token: route.token, + totalTransfers: route.totalTransfers, + successfulTransfers: route.successfulTransfers, + failedTransfers: route.failedTransfers, + successRate: Number(route.successRate.toFixed(2)), + averageSettlementTimeMs: route.averageSettlementTimeMs + ? Number(route.averageSettlementTimeMs) + : undefined, + averageFee: route.averageFee ? Number(route.averageFee) : undefined, + averageSlippagePercent: route.averageSlippagePercent + ? Number(route.averageSlippagePercent) + : undefined, + totalVolume: Number(route.totalVolume.toFixed(10)), + lastUpdated: route.lastUpdated, + }; + + if (includeRanking && recommendation) { + insight.recommendationRank = + recommendations.indexOf(recommendation) + 1; + insight.recommendationScore = Number(recommendation.score.toFixed(2)); + } + + insightsData.push(insight); + } + + // Sort by rank if available + if (includeRanking) { + insightsData.sort( + (a, b) => (a.recommendationRank ?? 999) - (b.recommendationRank ?? 999), + ); + } + + return insightsData; + } + + /** + * Generate CSV export + */ + private generateCsvExport( + exportId: string, + data: RouteInsightDataDto[], + delimiter: string, + userId: string, + ): RouteInsightsExportResponseDto { + const csv = this.buildCsv(data, delimiter); + const fileName = this.buildFileName( + 'stellar-route-insights', + ExportFormat.CSV, + ); + + this.logger.log( + `CSV export generated: id=${exportId}, user=${userId}, rows=${data.length}, size=${this.csvBuilder.estimateSize(csv)}B`, + ); + + return { + exportId, + format: ExportFormat.CSV, + rowCount: data.length, + downloadUrl: `/api/exporters/routes/stellar/download/${exportId}`, + generatedAt: new Date(), + }; + } + + /** + * Generate JSON export + */ + private generateJsonExport( + exportId: string, + data: RouteInsightDataDto[], + userId: string, + ): RouteInsightsExportResponseDto { + const jsonContent = JSON.stringify( + { + metadata: { + exportId, + exportedAt: new Date().toISOString(), + format: 'json', + rowCount: data.length, + }, + data, + }, + null, + 2, + ); + + const fileName = this.buildFileName( + 'stellar-route-insights', + ExportFormat.JSON, + ); + + this.logger.log( + `JSON export generated: id=${exportId}, user=${userId}, rows=${data.length}, size=${Buffer.byteLength(jsonContent, 'utf8')}B`, + ); + + return { + exportId, + format: ExportFormat.JSON, + rowCount: data.length, + downloadUrl: `/api/exporters/routes/stellar/download/${exportId}`, + generatedAt: new Date(), + }; + } + + /** + * Build CSV from insights data + */ + private buildCsv(data: RouteInsightDataDto[], delimiter: string): string { + const columns = [ + { key: 'bridgeName', header: 'Bridge Name' }, + { key: 'sourceChain', header: 'Source Chain' }, + { key: 'destinationChain', header: 'Destination Chain' }, + { key: 'token', header: 'Token' }, + { key: 'totalTransfers', header: 'Total Transfers' }, + { key: 'successfulTransfers', header: 'Successful Transfers' }, + { key: 'failedTransfers', header: 'Failed Transfers' }, + { key: 'successRate', header: 'Success Rate (%)' }, + { key: 'averageSettlementTimeMs', header: 'Avg Settlement Time (ms)' }, + { key: 'averageFee', header: 'Average Fee' }, + { key: 'averageSlippagePercent', header: 'Average Slippage (%)' }, + { key: 'totalVolume', header: 'Total Volume' }, + { key: 'recommendationRank', header: 'Recommendation Rank' }, + { key: 'recommendationScore', header: 'Recommendation Score' }, + { key: 'lastUpdated', header: 'Last Updated' }, + ]; + + const buildOptions = { + columns, + delimiter, + includeHeader: true, + nullPlaceholder: '', + }; + + return this.csvBuilder.build(data as any, buildOptions); + } + + /** + * Build analytics query from DTO + */ + private buildAnalyticsQuery( + dto: RouteInsightsExportDto, + ): BridgeAnalyticsQueryDto { + return { + sourceChain: dto.sourceChain, + destinationChain: dto.destinationChain, + bridgeName: dto.bridgeName, + token: dto.token, + startDate: dto.startDate, + endDate: dto.endDate, + limit: 1000, // High limit for analytics + }; + } + + /** + * Build file name for export + */ + private buildFileName(prefix: string, format: ExportFormat): string { + const timestamp = new Date().toISOString().split('T')[0]; + const ext = format === ExportFormat.CSV ? 'csv' : 'json'; + return `${prefix}_${timestamp}.${ext}`; + } + + /** + * Get export status (for async exports in future) + */ + async getExportStatus(exportId: string, userId: string): Promise { + // Future implementation for async export tracking + throw new NotFoundException(`Export ${exportId} not found`); + } + + /** + * List user exports (for future) + */ + async listUserExports(userId: string): Promise { + // Future implementation + return []; + } +} diff --git a/package-lock.json b/package-lock.json index 94d56c5f..2656bc30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -250,6 +250,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2359,6 +2360,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -2529,6 +2531,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", "license": "MIT", + "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -2576,6 +2579,7 @@ "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -2649,6 +2653,7 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", "license": "MIT", + "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3000,7 +3005,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -3021,7 +3025,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -3035,7 +3038,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3050,8 +3052,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", @@ -3454,6 +3455,7 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3483,6 +3485,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3623,6 +3626,7 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -4061,6 +4065,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4132,6 +4137,7 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4374,6 +4380,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -4613,6 +4620,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4828,6 +4836,7 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -4874,13 +4883,15 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", + "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -5806,6 +5817,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5866,6 +5878,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6137,6 +6150,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7591,6 +7605,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -10029,6 +10044,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -10286,6 +10302,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10468,6 +10485,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10480,6 +10498,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10540,7 +10559,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -10706,6 +10726,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -11531,6 +11552,7 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11927,6 +11949,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12093,6 +12116,7 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", + "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -12288,6 +12312,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12646,7 +12671,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12665,7 +12689,6 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12679,7 +12702,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12694,7 +12716,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -12704,8 +12725,7 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "4.3.3", @@ -12713,7 +12733,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0",