Browse Source

Task/move market data endpoint for single asset to asset profiles (#7079)

* Move market data endpoint for single asset to asset profiles

* Update changelog
pull/7086/head
Akash Negi 2 days ago
committed by GitHub
parent
commit
04a1c75365
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/admin/admin.module.ts
  3. 63
      apps/api/src/app/admin/admin.service.ts
  4. 11
      apps/api/src/app/asset/asset.controller.ts
  5. 4
      apps/api/src/app/asset/asset.module.ts
  6. 58
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.controller.ts
  7. 11
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  8. 61
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.service.ts
  9. 55
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  10. 12
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  11. 4
      libs/common/src/lib/interfaces/index.ts
  12. 2
      libs/common/src/lib/interfaces/responses/asset-profile-response.interface.ts
  13. 7
      libs/ui/src/lib/services/data.service.ts

1
CHANGELOG.md

@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed the _Fear & Greed Index_ (market mood) in the markets overview to use the stored market data instead of a live quote - Changed the _Fear & Greed Index_ (market mood) in the markets overview to use the stored market data instead of a live quote
- Moved the endpoint to get the asset profiles from `GET api/v1/admin/market-data` to `GET api/v1/asset-profiles` - Moved the endpoint to get the asset profiles from `GET api/v1/admin/market-data` to `GET api/v1/asset-profiles`
- Moved the endpoint to get the asset profile details from `GET api/v1/market-data/:dataSource/:symbol` to `GET api/v1/asset-profiles/:dataSource/:symbol`
- Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel - Added the selected asset profile count to the delete menu item of the historical market data table in the admin control panel
- Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel - Added the selected asset profile count to the deletion confirmation dialog of the historical market data table in the admin control panel
- Improved the sorting to be case-insensitive in the platform management of the admin control panel - Improved the sorting to be case-insensitive in the platform management of the admin control panel

2
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 { 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 { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -19,7 +18,6 @@ import { QueueModule } from './queue/queue.module';
@Module({ @Module({
imports: [ imports: [
ActivitiesModule,
BenchmarkModule, BenchmarkModule,
ConfigurationModule, ConfigurationModule,
DataGatheringQueueModule, DataGatheringQueueModule,

63
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 { environment } from '@ghostfolio/api/environments/environment';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
@ -12,14 +11,12 @@ import {
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_IS_USER_SIGNUP_ENABLED PROPERTY_IS_USER_SIGNUP_ENABLED
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { getCurrencyFromSymbol } from '@ghostfolio/common/helper';
import { import {
AdminData, AdminData,
AdminMarketDataDetails,
AdminUserResponse, AdminUserResponse,
AdminUsersResponse, AdminUsersResponse,
AssetProfileIdentifier, AssetProfileIdentifier
EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
@ -42,7 +39,6 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
public constructor( public constructor(
private readonly activitiesService: ActivitiesService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
@ -176,61 +172,6 @@ export class AdminService {
}; };
} }
public async getMarketDataBySymbol({
dataSource,
symbol
}: AssetProfileIdentifier): Promise<AdminMarketDataDetails> {
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<AdminUserResponse> { public async getUser(id: string): Promise<AdminUserResponse> {
const [user] = await this.getUsersWithAnalytics({ const [user] = await this.getUsersWithAnalytics({
where: { id } where: { id }

11
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 { 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 { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AssetResponse } from '@ghostfolio/common/interfaces'; import type { AssetResponse } from '@ghostfolio/common/interfaces';
@ -9,7 +9,9 @@ import { pick } from 'lodash';
@Controller('asset') @Controller('asset')
export class AssetController { export class AssetController {
public constructor(private readonly adminService: AdminService) {} public constructor(
private readonly assetProfilesService: AssetProfilesService
) {}
@Get(':dataSource/:symbol') @Get(':dataSource/:symbol')
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@ -19,7 +21,10 @@ export class AssetController {
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<AssetResponse> { ): Promise<AssetResponse> {
const { assetProfile, marketData } = const { assetProfile, marketData } =
await this.adminService.getMarketDataBySymbol({ dataSource, symbol }); await this.assetProfilesService.getAssetProfile({
dataSource,
symbol
});
return { return {
marketData, marketData,

4
apps/api/src/app/asset/asset.module.ts

@ -1,4 +1,4 @@
import { AdminModule } from '@ghostfolio/api/app/admin/admin.module'; import { AssetProfilesModule } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.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 { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -9,7 +9,7 @@ import { AssetController } from './asset.controller';
@Module({ @Module({
controllers: [AssetController], controllers: [AssetController],
imports: [ imports: [
AdminModule, AssetProfilesModule,
TransformDataSourceInRequestModule, TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule TransformDataSourceInResponseModule
] ]

58
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 { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { 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 { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { AssetProfileResponse } from '@ghostfolio/common/interfaces';
import { import {
AssetProfilesResponse, AssetProfilesResponse,
EnhancedSymbolProfile EnhancedSymbolProfile
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission } from '@ghostfolio/common/permissions';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types'; import { MarketDataPreset, RequestWithUser } from '@ghostfolio/common/types';
@ -18,7 +24,8 @@ import {
Param, Param,
Patch, Patch,
Query, Query,
UseGuards UseGuards,
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -32,7 +39,8 @@ export class AssetProfilesController {
public constructor( public constructor(
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly assetProfilesService: AssetProfilesService, private readonly assetProfilesService: AssetProfilesService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@Get() @Get()
@ -64,6 +72,52 @@ export class AssetProfilesController {
}); });
} }
@Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAssetProfile(
@Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string
): Promise<AssetProfileResponse> {
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.getAssetProfile({
dataSource,
symbol
});
}
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)
@Patch(':dataSource/:symbol') @Patch(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)

11
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 { 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 { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.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 { 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 { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -12,13 +16,18 @@ import { AssetProfilesService } from './asset-profiles.service';
@Module({ @Module({
controllers: [AssetProfilesController], controllers: [AssetProfilesController],
exports: [AssetProfilesService],
imports: [ imports: [
ActivitiesModule, ActivitiesModule,
ApiModule, ApiModule,
BenchmarkModule, BenchmarkModule,
DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
MarketDataModule,
PrismaModule, PrismaModule,
SymbolProfileModule SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
], ],
providers: [AssetProfilesService] providers: [AssetProfilesService]
}) })

61
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 { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.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 { 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 { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos';
@ -11,8 +13,9 @@ import {
isCurrency isCurrency
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
AssetProfileItem, AdminMarketDataDetails,
AssetProfileIdentifier, AssetProfileIdentifier,
AssetProfileItem,
AssetProfilesResponse, AssetProfilesResponse,
EnhancedSymbolProfile, EnhancedSymbolProfile,
Filter Filter
@ -28,11 +31,67 @@ export class AssetProfilesService {
public constructor( public constructor(
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async getAssetProfile({
dataSource,
symbol
}: AssetProfileIdentifier): Promise<AdminMarketDataDetails> {
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({ public async getAssetProfiles({
filters = [], filters = [],
presetId, presetId,

55
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 { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; 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 { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -14,10 +11,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos'; import { UpdateBulkMarketDataDto } from '@ghostfolio/common/dtos';
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper'; import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
import { import { MarketDataOfMarketsResponse } from '@ghostfolio/common/interfaces';
MarketDataDetailsResponse,
MarketDataOfMarketsResponse
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { RequestWithUser } from '@ghostfolio/common/types'; import { RequestWithUser } from '@ghostfolio/common/types';
@ -30,8 +24,7 @@ import {
Param, Param,
Post, Post,
Query, Query,
UseGuards, UseGuards
UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
@ -42,7 +35,6 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@Controller('market-data') @Controller('market-data')
export class MarketDataController { export class MarketDataController {
public constructor( public constructor(
private readonly adminService: AdminService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService, private readonly symbolProfileService: SymbolProfileService,
@ -89,49 +81,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<MarketDataDetailsResponse> {
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') @Post(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt')) @UseGuards(AuthGuard('jwt'))
public async updateMarketData( public async updateMarketData(

12
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 { 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 { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -11,13 +8,6 @@ import { MarketDataController } from './market-data.controller';
@Module({ @Module({
controllers: [MarketDataController], controllers: [MarketDataController],
imports: [ imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule]
AdminModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
}) })
export class MarketDataModule {} export class MarketDataModule {}

4
libs/common/src/lib/interfaces/index.ts

@ -47,6 +47,7 @@ import type { AdminUsersResponse } from './responses/admin-users-response.interf
import type { AiPromptResponse } from './responses/ai-prompt-response.interface'; import type { AiPromptResponse } from './responses/ai-prompt-response.interface';
import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface'; import type { AiServiceHealthResponse } from './responses/ai-service-health-response.interface';
import type { ApiKeyResponse } from './responses/api-key-response.interface'; import type { ApiKeyResponse } from './responses/api-key-response.interface';
import type { AssetProfileResponse } from './responses/asset-profile-response.interface';
import type { AssetProfilesResponse } from './responses/asset-profiles-response.interface'; import type { AssetProfilesResponse } from './responses/asset-profiles-response.interface';
import type { AssetResponse } from './responses/asset-response.interface'; import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface'; import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
@ -67,7 +68,6 @@ import type { HistoricalResponse } from './responses/historical-response.interfa
import type { ImportResponse } from './responses/import-response.interface'; import type { ImportResponse } from './responses/import-response.interface';
import type { InfoResponse } from './responses/info-response.interface'; import type { InfoResponse } from './responses/info-response.interface';
import type { LookupResponse } from './responses/lookup-response.interface'; import type { LookupResponse } from './responses/lookup-response.interface';
import type { MarketDataDetailsResponse } from './responses/market-data-details-response.interface';
import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface'; import type { MarketDataOfMarketsResponse } from './responses/market-data-of-markets-response.interface';
import type { OAuthResponse } from './responses/oauth-response.interface'; import type { OAuthResponse } from './responses/oauth-response.interface';
import type { PlatformsResponse } from './responses/platforms-response.interface'; import type { PlatformsResponse } from './responses/platforms-response.interface';
@ -123,6 +123,7 @@ export {
AssetClassSelectorOption, AssetClassSelectorOption,
AssetProfileIdentifier, AssetProfileIdentifier,
AssetProfileItem, AssetProfileItem,
AssetProfileResponse,
AssetProfilesResponse, AssetProfilesResponse,
AssetResponse, AssetResponse,
AttestationCredentialJSON, AttestationCredentialJSON,
@ -158,7 +159,6 @@ export {
LookupItem, LookupItem,
LookupResponse, LookupResponse,
MarketData, MarketData,
MarketDataDetailsResponse,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
NullableLineChartItem, NullableLineChartItem,
OAuthResponse, OAuthResponse,

2
libs/common/src/lib/interfaces/responses/market-data-details-response.interface.ts → libs/common/src/lib/interfaces/responses/asset-profile-response.interface.ts

@ -2,7 +2,7 @@ import { MarketData } from '@prisma/client';
import { EnhancedSymbolProfile } from '../enhanced-symbol-profile.interface'; import { EnhancedSymbolProfile } from '../enhanced-symbol-profile.interface';
export interface MarketDataDetailsResponse { export interface AssetProfileResponse {
assetProfile: Partial<EnhancedSymbolProfile>; assetProfile: Partial<EnhancedSymbolProfile>;
marketData: MarketData[]; marketData: MarketData[];
} }

7
libs/ui/src/lib/services/data.service.ts

@ -28,6 +28,7 @@ import {
AiPromptResponse, AiPromptResponse,
ApiKeyResponse, ApiKeyResponse,
AssetProfileIdentifier, AssetProfileIdentifier,
AssetProfileResponse,
AssetProfilesResponse, AssetProfilesResponse,
AssetResponse, AssetResponse,
BenchmarkMarketDataDetailsResponse, BenchmarkMarketDataDetailsResponse,
@ -40,7 +41,6 @@ import {
ImportResponse, ImportResponse,
InfoItem, InfoItem,
LookupResponse, LookupResponse,
MarketDataDetailsResponse,
MarketDataOfMarketsResponse, MarketDataOfMarketsResponse,
OAuthResponse, OAuthResponse,
PlatformsResponse, PlatformsResponse,
@ -544,14 +544,15 @@ export class DataService {
}: { }: {
dataSource: DataSource; dataSource: DataSource;
symbol: string; symbol: string;
}): Observable<MarketDataDetailsResponse> { }): Observable<AssetProfileResponse> {
return this.http return this.http
.get<any>(`/api/v1/market-data/${dataSource}/${symbol}`) .get<any>(`/api/v1/asset-profiles/${dataSource}/${symbol}`)
.pipe( .pipe(
map((data) => { map((data) => {
for (const item of data.marketData) { for (const item of data.marketData) {
item.date = parseISO(item.date); item.date = parseISO(item.date);
} }
return data; return data;
}) })
); );

Loading…
Cancel
Save