From 3bbc5e1d42122e123ccf9aa26a7ffed81a61ddea Mon Sep 17 00:00:00 2001 From: AkashNegi1 Date: Sat, 20 Jun 2026 15:01:31 +0530 Subject: [PATCH] Task/move market data endpoint to asset profiles (#7026 follow-up) --- apps/api/src/app/admin/admin.module.ts | 2 - apps/api/src/app/admin/admin.service.ts | 63 +------------------ apps/api/src/app/asset/asset.controller.ts | 11 +++- apps/api/src/app/asset/asset.module.ts | 4 +- .../asset-profiles.controller.ts | 56 ++++++++++++++++- .../asset-profiles/asset-profiles.module.ts | 13 +++- .../asset-profiles/asset-profiles.service.ts | 59 +++++++++++++++++ .../market-data/market-data.controller.ts | 55 +--------------- .../market-data/market-data.module.ts | 12 +--- libs/ui/src/lib/services/data.service.ts | 2 +- 10 files changed, 141 insertions(+), 136 deletions(-) diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index 0cd4d3c16..2d5c734dc 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -1,4 +1,3 @@ -import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; @@ -19,7 +18,6 @@ import { QueueModule } from './queue/queue.module'; @Module({ imports: [ - ActivitiesModule, BenchmarkModule, ConfigurationModule, DataGatheringQueueModule, diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 46ab25e96..93e77aff7 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -1,4 +1,3 @@ -import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { environment } from '@ghostfolio/api/environments/environment'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; @@ -12,14 +11,12 @@ import { PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_USER_SIGNUP_ENABLED } from '@ghostfolio/common/config'; -import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; +import { getCurrencyFromSymbol } from '@ghostfolio/common/helper'; import { AdminData, - AdminMarketDataDetails, AdminUserResponse, AdminUsersResponse, - AssetProfileIdentifier, - EnhancedSymbolProfile + AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { @@ -42,7 +39,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @Injectable() export class AdminService { public constructor( - private readonly activitiesService: ActivitiesService, private readonly configurationService: ConfigurationService, private readonly dataProviderService: DataProviderService, private readonly exchangeRateDataService: ExchangeRateDataService, @@ -176,61 +172,6 @@ export class AdminService { }; } - public async getMarketDataBySymbol({ - dataSource, - symbol - }: AssetProfileIdentifier): Promise { - let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0; - let currency: EnhancedSymbolProfile['currency'] = '-'; - let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity']; - - const isCurrencyAssetProfile = isCurrency(getCurrencyFromSymbol(symbol)); - - if (isCurrencyAssetProfile) { - currency = getCurrencyFromSymbol(symbol); - ({ activitiesCount, dateOfFirstActivity } = - await this.activitiesService.getStatisticsByCurrency(currency)); - } - - const [[assetProfile], marketData] = await Promise.all([ - this.symbolProfileService.getSymbolProfiles([ - { - dataSource, - symbol - } - ]), - this.marketDataService.marketDataItems({ - orderBy: { - date: 'asc' - }, - where: { - dataSource, - symbol - } - }) - ]); - - if (assetProfile) { - assetProfile.dataProviderInfo = this.dataProviderService - .getDataProvider(assetProfile.dataSource) - .getDataProviderInfo(); - } - - return { - marketData, - assetProfile: assetProfile ?? { - activitiesCount, - currency, - dataSource, - dateOfFirstActivity, - symbol, - assetClass: isCurrencyAssetProfile ? AssetClass.LIQUIDITY : undefined, - assetSubClass: isCurrencyAssetProfile ? AssetSubClass.CASH : undefined, - isActive: true - } - }; - } - public async getUser(id: string): Promise { const [user] = await this.getUsersWithAnalytics({ where: { id } diff --git a/apps/api/src/app/asset/asset.controller.ts b/apps/api/src/app/asset/asset.controller.ts index 3b2031084..575916b38 100644 --- a/apps/api/src/app/asset/asset.controller.ts +++ b/apps/api/src/app/asset/asset.controller.ts @@ -1,4 +1,4 @@ -import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; +import { AssetProfilesService } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.service'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import type { AssetResponse } from '@ghostfolio/common/interfaces'; @@ -9,7 +9,9 @@ import { pick } from 'lodash'; @Controller('asset') export class AssetController { - public constructor(private readonly adminService: AdminService) {} + public constructor( + private readonly assetProfilesService: AssetProfilesService + ) {} @Get(':dataSource/:symbol') @UseInterceptors(TransformDataSourceInRequestInterceptor) @@ -19,7 +21,10 @@ export class AssetController { @Param('symbol') symbol: string ): Promise { const { assetProfile, marketData } = - await this.adminService.getMarketDataBySymbol({ dataSource, symbol }); + await this.assetProfilesService.getMarketDataBySymbol({ + dataSource, + symbol + }); return { marketData, diff --git a/apps/api/src/app/asset/asset.module.ts b/apps/api/src/app/asset/asset.module.ts index 168585ed8..6c3d3ab7c 100644 --- a/apps/api/src/app/asset/asset.module.ts +++ b/apps/api/src/app/asset/asset.module.ts @@ -1,15 +1,15 @@ -import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { Module } from '@nestjs/common'; +import { AssetProfilesModule } from '../endpoints/asset-profiles/asset-profiles.module'; import { AssetController } from './asset.controller'; @Module({ controllers: [AssetController], imports: [ - AdminModule, + AssetProfilesModule, TransformDataSourceInRequestModule, TransformDataSourceInResponseModule ] diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts index 2606d8075..620178f0c 100644 --- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts @@ -1,11 +1,17 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; +import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { ApiService } from '@ghostfolio/api/services/api/api.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; +import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; +import { MarketDataDetailsResponse } from '@ghostfolio/common/interfaces'; import { AssetProfilesResponse, EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; +import { hasPermission } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions'; import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types'; @@ -18,7 +24,8 @@ import { Param, Patch, Query, - UseGuards + UseGuards, + UseInterceptors } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -32,6 +39,7 @@ export class AssetProfilesController { public constructor( private readonly apiService: ApiService, private readonly assetProfilesService: AssetProfilesService, + private readonly symbolProfileService: SymbolProfileService, @Inject(REQUEST) private readonly request: RequestWithUser ) {} @@ -64,6 +72,52 @@ export class AssetProfilesController { }); } + @Get(':dataSource/:symbol') + @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getAssetProfileMarketData( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ + { dataSource, symbol } + ]); + + if (!assetProfile && !isCurrency(getCurrencyFromSymbol(symbol))) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + const canReadAllAssetProfiles = hasPermission( + this.request.user.permissions, + permissions.readMarketData + ); + + const canReadOwnAssetProfile = + assetProfile?.userId === this.request.user.id && + hasPermission( + this.request.user.permissions, + permissions.readMarketDataOfOwnAssetProfile + ); + + if (!canReadAllAssetProfiles && !canReadOwnAssetProfile) { + throw new HttpException( + assetProfile?.userId + ? getReasonPhrase(StatusCodes.NOT_FOUND) + : getReasonPhrase(StatusCodes.FORBIDDEN), + assetProfile?.userId ? StatusCodes.NOT_FOUND : StatusCodes.FORBIDDEN + ); + } + + return this.assetProfilesService.getMarketDataBySymbol({ + dataSource, + symbol + }); + } + @HasPermission(permissions.accessAdminControl) @Patch(':dataSource/:symbol') @UseGuards(AuthGuard('jwt'), HasPermissionGuard) diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts index 98463ce5d..2c0ef7f8d 100644 --- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts @@ -1,7 +1,11 @@ import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; +import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; +import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; +import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; +import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -16,10 +20,15 @@ import { AssetProfilesService } from './asset-profiles.service'; ActivitiesModule, ApiModule, BenchmarkModule, + DataProviderModule, ExchangeRateDataModule, + MarketDataModule, PrismaModule, - SymbolProfileModule + SymbolProfileModule, + TransformDataSourceInRequestModule, + TransformDataSourceInResponseModule ], - providers: [AssetProfilesService] + providers: [AssetProfilesService], + exports: [AssetProfilesService] }) export class AssetProfilesModule {} diff --git a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts index 39eaa1642..43881d33c 100644 --- a/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts +++ b/apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts @@ -1,6 +1,8 @@ import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; +import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; @@ -14,6 +16,7 @@ import { AssetProfileItem, AssetProfileIdentifier, AssetProfilesResponse, + AdminMarketDataDetails, EnhancedSymbolProfile, Filter } from '@ghostfolio/common/interfaces'; @@ -29,10 +32,66 @@ export class AssetProfilesService { private readonly activitiesService: ActivitiesService, private readonly benchmarkService: BenchmarkService, private readonly exchangeRateDataService: ExchangeRateDataService, + private readonly dataProviderService: DataProviderService, + private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly symbolProfileService: SymbolProfileService ) {} + public async getMarketDataBySymbol({ + dataSource, + symbol + }: AssetProfileIdentifier): Promise { + let activitiesCount: EnhancedSymbolProfile['activitiesCount'] = 0; + let currency: EnhancedSymbolProfile['currency'] = '-'; + let dateOfFirstActivity: EnhancedSymbolProfile['dateOfFirstActivity']; + + const isCurrencyAssetProfile = isCurrency(getCurrencyFromSymbol(symbol)); + + if (isCurrencyAssetProfile) { + currency = getCurrencyFromSymbol(symbol); + ({ activitiesCount, dateOfFirstActivity } = + await this.activitiesService.getStatisticsByCurrency(currency)); + } + + const [[assetProfile], marketData] = await Promise.all([ + this.symbolProfileService.getSymbolProfiles([ + { + dataSource, + symbol + } + ]), + this.marketDataService.marketDataItems({ + orderBy: { + date: 'asc' + }, + where: { + dataSource, + symbol + } + }) + ]); + + if (assetProfile) { + assetProfile.dataProviderInfo = this.dataProviderService + .getDataProvider(assetProfile.dataSource) + .getDataProviderInfo(); + } + + return { + marketData, + assetProfile: assetProfile ?? { + activitiesCount, + currency, + dataSource, + dateOfFirstActivity, + symbol, + assetClass: isCurrencyAssetProfile ? AssetClass.LIQUIDITY : undefined, + assetSubClass: isCurrencyAssetProfile ? AssetSubClass.CASH : undefined, + isActive: true + } + }; + } public async getAssetProfiles({ filters = [], presetId, diff --git a/apps/api/src/app/endpoints/market-data/market-data.controller.ts b/apps/api/src/app/endpoints/market-data/market-data.controller.ts index f6857283b..cbd0a598f 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.controller.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.controller.ts @@ -1,9 +1,6 @@ -import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; -import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; -import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { @@ -14,10 +11,7 @@ import { } from '@ghostfolio/common/config'; import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; -import { - MarketDataDetailsResponse, - MarketDataOfMarketsResponse -} from '@ghostfolio/common/interfaces'; +import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { RequestWithUser } from '@ghostfolio/common/types'; @@ -30,8 +24,7 @@ import { Param, Post, Query, - UseGuards, - UseInterceptors + UseGuards } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -42,7 +35,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes'; @Controller('market-data') export class MarketDataController { public constructor( - private readonly adminService: AdminService, private readonly marketDataService: MarketDataService, @Inject(REQUEST) private readonly request: RequestWithUser, private readonly symbolProfileService: SymbolProfileService, @@ -87,49 +79,6 @@ export class MarketDataController { }; } - @Get(':dataSource/:symbol') - @UseGuards(AuthGuard('jwt')) - @UseInterceptors(TransformDataSourceInRequestInterceptor) - @UseInterceptors(TransformDataSourceInResponseInterceptor) - public async getMarketDataBySymbol( - @Param('dataSource') dataSource: DataSource, - @Param('symbol') symbol: string - ): Promise { - const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ - { dataSource, symbol } - ]); - - if (!assetProfile && !isCurrency(getCurrencyFromSymbol(symbol))) { - throw new HttpException( - getReasonPhrase(StatusCodes.NOT_FOUND), - StatusCodes.NOT_FOUND - ); - } - - const canReadAllAssetProfiles = hasPermission( - this.request.user.permissions, - permissions.readMarketData - ); - - const canReadOwnAssetProfile = - assetProfile?.userId === this.request.user.id && - hasPermission( - this.request.user.permissions, - permissions.readMarketDataOfOwnAssetProfile - ); - - if (!canReadAllAssetProfiles && !canReadOwnAssetProfile) { - throw new HttpException( - assetProfile?.userId - ? getReasonPhrase(StatusCodes.NOT_FOUND) - : getReasonPhrase(StatusCodes.FORBIDDEN), - assetProfile?.userId ? StatusCodes.NOT_FOUND : StatusCodes.FORBIDDEN - ); - } - - return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); - } - @Post(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async updateMarketData( diff --git a/apps/api/src/app/endpoints/market-data/market-data.module.ts b/apps/api/src/app/endpoints/market-data/market-data.module.ts index d5d64673d..1de10907b 100644 --- a/apps/api/src/app/endpoints/market-data/market-data.module.ts +++ b/apps/api/src/app/endpoints/market-data/market-data.module.ts @@ -1,7 +1,4 @@ -import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; -import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; -import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; @@ -11,13 +8,6 @@ import { MarketDataController } from './market-data.controller'; @Module({ controllers: [MarketDataController], - imports: [ - AdminModule, - MarketDataServiceModule, - SymbolModule, - SymbolProfileModule, - TransformDataSourceInRequestModule, - TransformDataSourceInResponseModule - ] + imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule] }) export class MarketDataModule {} diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index 079ea1bf6..6a3bed91f 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -546,7 +546,7 @@ export class DataService { symbol: string; }): Observable { return this.http - .get(`/api/v1/market-data/${dataSource}/${symbol}`) + .get(`/api/v1/asset-profiles/${dataSource}/${symbol}`) .pipe( map((data) => { for (const item of data.marketData) {