diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f02a4b..ca131064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added support for deleting symbol profile data in the admin control panel + ### Changed - Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e2452244..8ce4def5 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -11,6 +11,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, + Delete, Get, HttpException, Inject, @@ -195,9 +196,10 @@ export class AdminController { return this.adminService.getMarketData(); } - @Get('market-data/:symbol') + @Get('market-data/:dataSource/:symbol') @UseGuards(AuthGuard('jwt')) public async getMarketDataBySymbol( + @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { if ( @@ -212,7 +214,7 @@ export class AdminController { ); } - return this.adminService.getMarketDataBySymbol(symbol); + return this.adminService.getMarketDataBySymbol({ dataSource, symbol }); } @Put('market-data/:dataSource/:symbol/:dateString') @@ -248,6 +250,27 @@ export class AdminController { }); } + @Delete('profile-data/:dataSource/:symbol') + @UseGuards(AuthGuard('jwt')) + public async deleteProfileData( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ): Promise { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } + + return this.adminService.deleteProfileData({ dataSource, symbol }); + } + @Put('settings/:key') @UseGuards(AuthGuard('jwt')) public async updateProperty( diff --git a/apps/api/src/app/admin/admin.module.ts b/apps/api/src/app/admin/admin.module.ts index b8e96fa0..8e83236e 100644 --- a/apps/api/src/app/admin/admin.module.ts +++ b/apps/api/src/app/admin/admin.module.ts @@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d import { MarketDataModule } from '@ghostfolio/api/services/market-data.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; +import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { Module } from '@nestjs/common'; import { AdminController } from './admin.controller'; @@ -20,7 +21,8 @@ import { AdminService } from './admin.service'; MarketDataModule, PrismaModule, PropertyModule, - SubscriptionModule + SubscriptionModule, + SymbolProfileModule ], controllers: [AdminController], providers: [AdminService], diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 4df3ea09..9c1de05b 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -5,6 +5,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service'; +import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config'; import { AdminData, @@ -13,7 +14,7 @@ import { AdminMarketDataItem } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; -import { Property } from '@prisma/client'; +import { DataSource, Property } from '@prisma/client'; import { differenceInDays } from 'date-fns'; @Injectable() @@ -25,9 +26,21 @@ export class AdminService { private readonly marketDataService: MarketDataService, private readonly prismaService: PrismaService, private readonly propertyService: PropertyService, - private readonly subscriptionService: SubscriptionService + private readonly subscriptionService: SubscriptionService, + private readonly symbolProfileService: SymbolProfileService ) {} + public async deleteProfileData({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + await this.marketDataService.deleteMany({ dataSource, symbol }); + await this.symbolProfileService.delete({ dataSource, symbol }); + } + public async get(): Promise { return { dataGatheringProgress: @@ -121,16 +134,21 @@ export class AdminService { }; } - public async getMarketDataBySymbol( - aSymbol: string - ): Promise { + public async getMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): Promise { return { marketData: await this.marketDataService.marketDataItems({ orderBy: { date: 'asc' }, where: { - symbol: aSymbol + dataSource, + symbol } }) }; diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index 0e0a2688..582ee259 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/apps/api/src/services/market-data.service.ts @@ -9,6 +9,21 @@ import { DataSource, MarketData, Prisma } from '@prisma/client'; export class MarketDataService { public constructor(private readonly prismaService: PrismaService) {} + public async deleteMany({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.prismaService.marketData.deleteMany({ + where: { + dataSource, + symbol + } + }); + } + public async get({ date, symbol diff --git a/apps/api/src/services/symbol-profile.service.ts b/apps/api/src/services/symbol-profile.service.ts index 0a95fd6d..0b285733 100644 --- a/apps/api/src/services/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile.service.ts @@ -4,14 +4,26 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Injectable } from '@nestjs/common'; -import { Prisma, SymbolProfile } from '@prisma/client'; +import { DataSource, Prisma, SymbolProfile } from '@prisma/client'; import { continents, countries } from 'countries-list'; import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface'; @Injectable() export class SymbolProfileService { - constructor(private readonly prismaService: PrismaService) {} + public constructor(private readonly prismaService: PrismaService) {} + + public async delete({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + return this.prismaService.symbolProfile.delete({ + where: { dataSource_symbol: { dataSource, symbol } } + }); + } public async getSymbolProfiles( symbols: string[] diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index a8d96233..5dd3a3fd 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -20,6 +20,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-market-data.html' }) export class AdminMarketDataComponent implements OnDestroy, OnInit { + public currentDataSource: DataSource; public currentSymbol: string; public defaultDateFormat = DEFAULT_DATE_FORMAT; public marketData: AdminMarketDataItem[] = []; @@ -43,6 +44,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } + public onDeleteProfileData({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .deleteProfileData({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => {}); + } + public onGatherProfileDataBySymbol({ dataSource, symbol @@ -69,22 +83,33 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { .subscribe(() => {}); } - public setCurrentSymbol(aSymbol: string) { - this.marketDataDetails = []; - - if (this.currentSymbol === aSymbol) { - this.currentSymbol = ''; - } else { - this.currentSymbol = aSymbol; - - this.fetchAdminMarketDataBySymbol(this.currentSymbol); - } - } - public onMarketDataChanged(withRefresh: boolean = false) { if (withRefresh) { this.fetchAdminMarketData(); - this.fetchAdminMarketDataBySymbol(this.currentSymbol); + this.fetchAdminMarketDataBySymbol({ + dataSource: this.currentDataSource, + symbol: this.currentSymbol + }); + } + } + + public setCurrentProfile({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.marketDataDetails = []; + + if (this.currentSymbol === symbol) { + this.currentDataSource = undefined; + this.currentSymbol = ''; + } else { + this.currentDataSource = dataSource; + this.currentSymbol = symbol; + + this.fetchAdminMarketDataBySymbol({ dataSource, symbol }); } } @@ -104,9 +129,15 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { }); } - private fetchAdminMarketDataBySymbol(aSymbol: string) { - this.dataService - .fetchAdminMarketDataBySymbol(aSymbol) + private fetchAdminMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }) { + this.adminService + .fetchAdminMarketDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ marketData }) => { this.marketDataDetails = marketData; diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.html b/apps/client/src/app/components/admin-market-data/admin-market-data.html index d1e45bfc..3ff7435b 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.html +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.html @@ -16,7 +16,7 @@ {{ item.symbol }} {{ item.dataSource }} @@ -49,11 +49,19 @@ > Gather Profile Data + - + ( + `/api/admin/profile-data/${dataSource}/${symbol}` + ); + } + + public fetchAdminMarketDataBySymbol({ + dataSource, + symbol + }: { + dataSource: DataSource; + symbol: string; + }): Observable { + return this.http + .get(`/api/admin/market-data/${dataSource}/${symbol}`) + .pipe( + map((data) => { + for (const item of data.marketData) { + item.date = parseISO(item.date); + } + return data; + }) + ); + } + public gatherMax() { return this.http.post(`/api/admin/gather/max`, {}); } diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 8d614a54..bf03b3f5 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -18,7 +18,6 @@ import { Accounts, AdminData, AdminMarketData, - AdminMarketDataDetails, Export, InfoItem, PortfolioChart, @@ -69,19 +68,6 @@ export class DataService { return this.http.get('/api/admin/market-data'); } - public fetchAdminMarketDataBySymbol( - aSymbol: string - ): Observable { - return this.http.get(`/api/admin/market-data/${aSymbol}`).pipe( - map((data) => { - for (const item of data.marketData) { - item.date = parseISO(item.date); - } - return data; - }) - ); - } - public deleteAccess(aId: string) { return this.http.delete(`/api/access/${aId}`); }