From 323cfbfcaa7d3700af02508d0b63d231f08af34f Mon Sep 17 00:00:00 2001 From: Shaunak Das <51281688+shaun-ak@users.noreply.github.com> Date: Sat, 14 Sep 2024 14:23:39 +0530 Subject: [PATCH] Feature/introduce filters in account endpoint (#3764) * Introduce filters in acount endpoint * Integrate endpoint in holding detail dialog * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> --- CHANGELOG.md | 10 +++++++ .../api/src/app/account/account.controller.ts | 15 +++++++++- apps/api/src/app/account/account.module.ts | 2 ++ .../src/app/portfolio/portfolio.service.ts | 29 ++++++++++++++++--- .../holding-detail-dialog.component.ts | 24 +++++++++++---- apps/client/src/app/services/data.service.ts | 6 ++-- 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 273b5457e..b69e9703f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Introduced filters (`dataSource` and `symbol`) in the accounts endpoint + +### Changed + +- Switched to the accounts endpoint in the holding detail dialog + ## 2.107.1 - 2024-09-12 ### Fixed diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 594a733f7..d8c3dd002 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -3,6 +3,8 @@ import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.servic import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; +import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; +import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; import { @@ -26,6 +28,7 @@ import { Param, Post, Put, + Query, UseGuards, UseInterceptors } from '@nestjs/common'; @@ -44,6 +47,7 @@ export class AccountController { public constructor( private readonly accountBalanceService: AccountBalanceService, private readonly accountService: AccountService, + private readonly apiService: ApiService, private readonly impersonationService: ImpersonationService, private readonly portfolioService: PortfolioService, @Inject(REQUEST) private readonly request: RequestWithUser @@ -84,13 +88,22 @@ export class AccountController { @Get() @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) + @UseInterceptors(TransformDataSourceInRequestInterceptor) public async getAllAccounts( - @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, + @Query('dataSource') filterByDataSource?: string, + @Query('symbol') filterBySymbol?: string ): Promise { const impersonationUserId = await this.impersonationService.validateImpersonationId(impersonationId); + const filters = this.apiService.buildFiltersFromQueryParams({ + filterByDataSource, + filterBySymbol + }); + return this.portfolioService.getAccountsWithAggregations({ + filters, userId: impersonationUserId || this.request.user.id, withExcludedAccounts: true }); diff --git a/apps/api/src/app/account/account.module.ts b/apps/api/src/app/account/account.module.ts index 1c2d20216..fb89bb2b6 100644 --- a/apps/api/src/app/account/account.module.ts +++ b/apps/api/src/app/account/account.module.ts @@ -1,6 +1,7 @@ import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; +import { ApiModule } from '@ghostfolio/api/services/api/api.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module'; import { ImpersonationModule } from '@ghostfolio/api/services/impersonation/impersonation.module'; @@ -16,6 +17,7 @@ import { AccountService } from './account.service'; exports: [AccountService], imports: [ AccountBalanceModule, + ApiModule, ConfigurationModule, ExchangeRateDataModule, ImpersonationModule, diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0cd602046..e1dfc888d 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -115,12 +115,33 @@ export class PortfolioService { }): Promise { const where: Prisma.AccountWhereInput = { userId }; - const accountFilter = filters?.find(({ type }) => { + const filterByAccount = filters?.find(({ type }) => { return type === 'ACCOUNT'; - }); + })?.id; + + const filterByDataSource = filters?.find(({ type }) => { + return type === 'DATA_SOURCE'; + })?.id; + + const filterBySymbol = filters?.find(({ type }) => { + return type === 'SYMBOL'; + })?.id; - if (accountFilter) { - where.id = accountFilter.id; + if (filterByAccount) { + where.id = filterByAccount; + } + + if (filterByDataSource && filterBySymbol) { + where.Order = { + some: { + SymbolProfile: { + AND: [ + { dataSource: filterByDataSource }, + { symbol: filterBySymbol } + ] + } + } + }; } const [accounts, details] = await Promise.all([ diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 70cd08874..8541f65ac 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -9,6 +9,7 @@ import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DataProviderInfo, EnhancedSymbolProfile, + Filter, LineChartItem, User } from '@ghostfolio/common/interfaces'; @@ -152,6 +153,11 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { tags: [] }); + const filters: Filter[] = [ + { id: this.data.dataSource, type: 'DATA_SOURCE' }, + { id: this.data.symbol, type: 'SYMBOL' } + ]; + this.tagsAvailable = tags.map(({ id, name }) => { return { id, @@ -173,12 +179,20 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { .subscribe(); }); + this.dataService + .fetchAccounts({ + filters + }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ accounts }) => { + this.accounts = accounts; + + this.changeDetectorRef.markForCheck(); + }); + this.dataService .fetchActivities({ - filters: [ - { id: this.data.dataSource, type: 'DATA_SOURCE' }, - { id: this.data.symbol, type: 'SYMBOL' } - ], + filters, sortColumn: this.sortColumn, sortDirection: this.sortDirection }) @@ -197,7 +211,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ - accounts, averagePrice, dataProviderInfo, dividendInBaseCurrency, @@ -219,7 +232,6 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { transactionCount, value }) => { - this.accounts = accounts; this.averagePrice = averagePrice; this.benchmarkDataItems = []; this.countries = {}; diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index cbdde0265..78373adcc 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -173,8 +173,10 @@ export class DataService { ); } - public fetchAccounts() { - return this.http.get('/api/v1/account'); + public fetchAccounts({ filters }: { filters?: Filter[] } = {}) { + const params = this.buildFiltersAsQueryParams({ filters }); + + return this.http.get('/api/v1/account', { params }); } public fetchActivities({