Feature/support deleting symbol profile data (#669)
* Add support for deleting symbol profile data * Update changelog
This commit is contained in:
parent
5d8bde5a70
commit
dc424a86ec
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for deleting symbol profile data in the admin control panel
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`)
|
- Used `dataSource` and `symbol` from `SymbolProfile` instead of the `order` object (in `ExportService` and `PortfolioService`)
|
||||||
|
@ -11,6 +11,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types';
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
@ -195,9 +196,10 @@ export class AdminController {
|
|||||||
return this.adminService.getMarketData();
|
return this.adminService.getMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('market-data/:symbol')
|
@Get('market-data/:dataSource/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getMarketDataBySymbol(
|
public async getMarketDataBySymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('symbol') symbol: string
|
@Param('symbol') symbol: string
|
||||||
): Promise<AdminMarketDataDetails> {
|
): Promise<AdminMarketDataDetails> {
|
||||||
if (
|
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')
|
@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<void> {
|
||||||
|
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')
|
@Put('settings/:key')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async updateProperty(
|
public async updateProperty(
|
||||||
|
@ -6,6 +6,7 @@ import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-d
|
|||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
import { PropertyModule } from '@ghostfolio/api/services/property/property.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 { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
@ -20,7 +21,8 @@ import { AdminService } from './admin.service';
|
|||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
PropertyModule,
|
PropertyModule,
|
||||||
SubscriptionModule
|
SubscriptionModule,
|
||||||
|
SymbolProfileModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
providers: [AdminService],
|
providers: [AdminService],
|
||||||
|
@ -5,6 +5,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.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 { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
AdminMarketDataItem
|
AdminMarketDataItem
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Property } from '@prisma/client';
|
import { DataSource, Property } from '@prisma/client';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,9 +26,21 @@ export class AdminService {
|
|||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly propertyService: PropertyService,
|
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<AdminData> {
|
public async get(): Promise<AdminData> {
|
||||||
return {
|
return {
|
||||||
dataGatheringProgress:
|
dataGatheringProgress:
|
||||||
@ -121,16 +134,21 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMarketDataBySymbol(
|
public async getMarketDataBySymbol({
|
||||||
aSymbol: string
|
dataSource,
|
||||||
): Promise<AdminMarketDataDetails> {
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}): Promise<AdminMarketDataDetails> {
|
||||||
return {
|
return {
|
||||||
marketData: await this.marketDataService.marketDataItems({
|
marketData: await this.marketDataService.marketDataItems({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
date: 'asc'
|
date: 'asc'
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
symbol: aSymbol
|
dataSource,
|
||||||
|
symbol
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
@ -9,6 +9,21 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
|
|||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
public constructor(private readonly prismaService: PrismaService) {}
|
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({
|
public async get({
|
||||||
date,
|
date,
|
||||||
symbol
|
symbol
|
||||||
|
@ -4,14 +4,26 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Injectable } from '@nestjs/common';
|
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 { continents, countries } from 'countries-list';
|
||||||
|
|
||||||
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SymbolProfileService {
|
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(
|
public async getSymbolProfiles(
|
||||||
symbols: string[]
|
symbols: string[]
|
||||||
|
@ -20,6 +20,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-market-data.html'
|
templateUrl: './admin-market-data.html'
|
||||||
})
|
})
|
||||||
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
||||||
|
public currentDataSource: DataSource;
|
||||||
public currentSymbol: string;
|
public currentSymbol: string;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
public marketData: AdminMarketDataItem[] = [];
|
public marketData: AdminMarketDataItem[] = [];
|
||||||
@ -43,6 +44,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
this.fetchAdminMarketData();
|
this.fetchAdminMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onDeleteProfileData({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.adminService
|
||||||
|
.deleteProfileData({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public onGatherProfileDataBySymbol({
|
public onGatherProfileDataBySymbol({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
@ -69,22 +83,33 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
.subscribe(() => {});
|
.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) {
|
public onMarketDataChanged(withRefresh: boolean = false) {
|
||||||
if (withRefresh) {
|
if (withRefresh) {
|
||||||
this.fetchAdminMarketData();
|
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) {
|
private fetchAdminMarketDataBySymbol({
|
||||||
this.dataService
|
dataSource,
|
||||||
.fetchAdminMarketDataBySymbol(aSymbol)
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.adminService
|
||||||
|
.fetchAdminMarketDataBySymbol({ dataSource, symbol })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ marketData }) => {
|
.subscribe(({ marketData }) => {
|
||||||
this.marketDataDetails = marketData;
|
this.marketDataDetails = marketData;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<ng-container *ngFor="let item of marketData; let i = index">
|
<ng-container *ngFor="let item of marketData; let i = index">
|
||||||
<tr
|
<tr
|
||||||
class="cursor-pointer mat-row"
|
class="cursor-pointer mat-row"
|
||||||
(click)="setCurrentSymbol(item.symbol)"
|
(click)="setCurrentProfile({ dataSource: item.dataSource, symbol: item.symbol })"
|
||||||
>
|
>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
<td class="mat-cell px-1 py-2">{{ item.symbol }}</td>
|
||||||
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
<td class="mat-cell px-1 py-2">{{ item.dataSource }}</td>
|
||||||
@ -49,11 +49,19 @@
|
|||||||
>
|
>
|
||||||
Gather Profile Data
|
Gather Profile Data
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
[disabled]="item.activityCount !== 0"
|
||||||
|
(click)="onDeleteProfileData({dataSource: item.dataSource, symbol: item.symbol})"
|
||||||
|
>
|
||||||
|
Delete Profile Data
|
||||||
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
<tr *ngIf="currentSymbol === item.symbol" class="mat-row">
|
||||||
<td class="p-1" colspan="4">
|
<td class="p-1" colspan="6">
|
||||||
<gf-admin-market-data-detail
|
<gf-admin-market-data-detail
|
||||||
[dataSource]="item.dataSource"
|
[dataSource]="item.dataSource"
|
||||||
[marketData]="marketDataDetails"
|
[marketData]="marketDataDetails"
|
||||||
|
@ -3,8 +3,10 @@ import { Injectable } from '@angular/core';
|
|||||||
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||||
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces';
|
||||||
import { DataSource, MarketData } from '@prisma/client';
|
import { DataSource, MarketData } from '@prisma/client';
|
||||||
import { format } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -12,6 +14,37 @@ import { format } from 'date-fns';
|
|||||||
export class AdminService {
|
export class AdminService {
|
||||||
public constructor(private http: HttpClient) {}
|
public constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
public deleteProfileData({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.http.delete<void>(
|
||||||
|
`/api/admin/profile-data/${dataSource}/${symbol}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public fetchAdminMarketDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}): Observable<AdminMarketDataDetails> {
|
||||||
|
return this.http
|
||||||
|
.get<any>(`/api/admin/market-data/${dataSource}/${symbol}`)
|
||||||
|
.pipe(
|
||||||
|
map((data) => {
|
||||||
|
for (const item of data.marketData) {
|
||||||
|
item.date = parseISO(item.date);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public gatherMax() {
|
public gatherMax() {
|
||||||
return this.http.post<void>(`/api/admin/gather/max`, {});
|
return this.http.post<void>(`/api/admin/gather/max`, {});
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import {
|
|||||||
Accounts,
|
Accounts,
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
AdminMarketDataDetails,
|
|
||||||
Export,
|
Export,
|
||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
@ -69,19 +68,6 @@ export class DataService {
|
|||||||
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
return this.http.get<AdminMarketData>('/api/admin/market-data');
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchAdminMarketDataBySymbol(
|
|
||||||
aSymbol: string
|
|
||||||
): Observable<AdminMarketDataDetails> {
|
|
||||||
return this.http.get<any>(`/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) {
|
public deleteAccess(aId: string) {
|
||||||
return this.http.delete<any>(`/api/access/${aId}`);
|
return this.http.delete<any>(`/api/access/${aId}`);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user