mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
* Move market data management from admin to dedicated endpoint * Update changelogpull/4126/head^2
Thomas Kaul
1 month ago
committed by
GitHub
15 changed files with 260 additions and 45 deletions
@ -0,0 +1,136 @@ |
|||
import { AdminService } from '@ghostfolio/api/app/admin/admin.service'; |
|||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
|||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
|||
import { MarketDataDetailsResponse } from '@ghostfolio/common/interfaces'; |
|||
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
|||
import { RequestWithUser } from '@ghostfolio/common/types'; |
|||
|
|||
import { |
|||
Body, |
|||
Controller, |
|||
Get, |
|||
HttpException, |
|||
Inject, |
|||
Param, |
|||
Post, |
|||
UseGuards |
|||
} from '@nestjs/common'; |
|||
import { REQUEST } from '@nestjs/core'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
import { DataSource, Prisma } from '@prisma/client'; |
|||
import { parseISO } from 'date-fns'; |
|||
import { getReasonPhrase, StatusCodes } from 'http-status-codes'; |
|||
|
|||
import { UpdateBulkMarketDataDto } from './update-bulk-market-data.dto'; |
|||
|
|||
@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 |
|||
) {} |
|||
|
|||
@Get(':dataSource/:symbol') |
|||
@UseGuards(AuthGuard('jwt')) |
|||
public async getMarketDataBySymbol( |
|||
@Param('dataSource') dataSource: DataSource, |
|||
@Param('symbol') symbol: string |
|||
): Promise<MarketDataDetailsResponse> { |
|||
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ |
|||
{ dataSource, symbol } |
|||
]); |
|||
|
|||
if (!assetProfile) { |
|||
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( |
|||
@Body() data: UpdateBulkMarketDataDto, |
|||
@Param('dataSource') dataSource: DataSource, |
|||
@Param('symbol') symbol: string |
|||
) { |
|||
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ |
|||
{ dataSource, symbol } |
|||
]); |
|||
|
|||
if (!assetProfile) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.NOT_FOUND), |
|||
StatusCodes.NOT_FOUND |
|||
); |
|||
} |
|||
|
|||
const canUpsertAllAssetProfiles = |
|||
hasPermission( |
|||
this.request.user.permissions, |
|||
permissions.createMarketData |
|||
) && |
|||
hasPermission( |
|||
this.request.user.permissions, |
|||
permissions.updateMarketData |
|||
); |
|||
|
|||
const canUpsertOwnAssetProfile = |
|||
assetProfile.userId === this.request.user.id && |
|||
hasPermission( |
|||
this.request.user.permissions, |
|||
permissions.createMarketDataOfOwnAssetProfile |
|||
) && |
|||
hasPermission( |
|||
this.request.user.permissions, |
|||
permissions.updateMarketDataOfOwnAssetProfile |
|||
); |
|||
|
|||
if (!canUpsertAllAssetProfiles && !canUpsertOwnAssetProfile) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.FORBIDDEN), |
|||
StatusCodes.FORBIDDEN |
|||
); |
|||
} |
|||
|
|||
const dataBulkUpdate: Prisma.MarketDataUpdateInput[] = data.marketData.map( |
|||
({ date, marketPrice }) => ({ |
|||
dataSource, |
|||
marketPrice, |
|||
symbol, |
|||
date: parseISO(date), |
|||
state: 'CLOSE' |
|||
}) |
|||
); |
|||
|
|||
return this.marketDataService.updateMany({ |
|||
data: dataBulkUpdate |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
import { AdminModule } from '@ghostfolio/api/app/admin/admin.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 { Module } from '@nestjs/common'; |
|||
|
|||
import { MarketDataController } from './market-data.controller'; |
|||
|
|||
@Module({ |
|||
controllers: [MarketDataController], |
|||
imports: [AdminModule, MarketDataServiceModule, SymbolProfileModule] |
|||
}) |
|||
export class MarketDataModule {} |
@ -0,0 +1,24 @@ |
|||
import { Type } from 'class-transformer'; |
|||
import { |
|||
ArrayNotEmpty, |
|||
IsArray, |
|||
IsISO8601, |
|||
IsNumber, |
|||
IsOptional |
|||
} from 'class-validator'; |
|||
|
|||
export class UpdateBulkMarketDataDto { |
|||
@ArrayNotEmpty() |
|||
@IsArray() |
|||
@Type(() => UpdateMarketDataDto) |
|||
marketData: UpdateMarketDataDto[]; |
|||
} |
|||
|
|||
class UpdateMarketDataDto { |
|||
@IsISO8601() |
|||
@IsOptional() |
|||
date?: string; |
|||
|
|||
@IsNumber() |
|||
marketPrice: number; |
|||
} |
@ -0,0 +1,8 @@ |
|||
import { MarketData } from '@prisma/client'; |
|||
|
|||
import { EnhancedSymbolProfile } from '../enhanced-symbol-profile.interface'; |
|||
|
|||
export interface MarketDataDetailsResponse { |
|||
assetProfile: Partial<EnhancedSymbolProfile>; |
|||
marketData: MarketData[]; |
|||
} |
Loading…
Reference in new issue