Browse Source

Task/destructure query parameters in controllers (part 2) (#7505)

Destructure query parameters in controllers
pull/7519/head
Thomas Kaul 6 days ago
committed by GitHub
parent
commit
0efadd7c8c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 30
      apps/api/src/app/activities/activities-filter.dto.ts
  2. 18
      apps/api/src/app/endpoints/ai/ai.controller.ts
  3. 37
      apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts
  4. 12
      apps/api/src/app/endpoints/benchmarks/get-benchmark-market-data.dto.ts
  5. 12
      apps/api/src/app/portfolio/get-details.dto.ts
  6. 10
      apps/api/src/app/portfolio/get-dividends.dto.ts
  7. 14
      apps/api/src/app/portfolio/get-holdings.dto.ts
  8. 10
      apps/api/src/app/portfolio/get-investments.dto.ts
  9. 12
      apps/api/src/app/portfolio/get-performance.dto.ts
  10. 159
      apps/api/src/app/portfolio/portfolio.controller.ts
  11. 16
      apps/api/src/dtos/date-range-filter.dto.ts
  12. 23
      apps/api/src/dtos/filter.dto.ts
  13. 3
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  14. 8
      libs/ui/src/lib/services/data.service.ts

30
apps/api/src/app/activities/activities-filter.dto.ts

@ -1,19 +1,13 @@
import { DATE_RANGES } from '@ghostfolio/common/config';
import { DATE_RANGE_PATTERN } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { FilterDto } from '@ghostfolio/api/dtos/filter.dto';
import { DateRange } from '@ghostfolio/common/types';
import { Type as ActivityType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsEnum, IsOptional, IsString, Matches } from 'class-validator';
import { IsEnum, IsOptional, Matches } from 'class-validator';
import { isString } from 'lodash';
// A named date range or a calendar year like '2024', '2023', '2022', etc.
const DATE_RANGE_PATTERN = new RegExp(`^(${DATE_RANGES.join('|')}|\\d{4})$`);
export class ActivitiesFilterDto {
@IsOptional()
@IsString()
accounts?: string;
export class ActivitiesFilterDto extends FilterDto {
@IsEnum(ActivityType, { each: true })
@IsOptional()
@Transform(({ value }: TransformFnParams) => {
@ -21,23 +15,7 @@ export class ActivitiesFilterDto {
})
activityTypes?: ActivityType[];
@IsOptional()
@IsString()
assetClasses?: string;
@IsOptional()
@IsString()
dataSource?: string;
@IsOptional()
@Matches(DATE_RANGE_PATTERN)
range?: DateRange;
@IsOptional()
@IsString()
symbol?: string;
@IsOptional()
@IsString()
tags?: string;
}

18
apps/api/src/app/endpoints/ai/ai.controller.ts

@ -1,4 +1,5 @@
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { FilterDto } from '@ghostfolio/api/dtos/filter.dto';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { AiPromptResponse } from '@ghostfolio/common/interfaces';
@ -31,18 +32,15 @@ export class AiController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getPrompt(
@Param('mode') mode: AiPromptMode,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
@Query()
{ accounts, assetClasses, dataSource, symbol, tags }: FilterDto
): Promise<AiPromptResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByDataSource: dataSource,
filterBySymbol: symbol,
filterByTags: tags
});
const prompt = await this.aiService.getPrompt({

37
apps/api/src/app/endpoints/benchmarks/benchmarks.controller.ts

@ -5,17 +5,14 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
DEFAULT_DATE_RANGE,
HEADER_KEY_IMPERSONATION
} from '@ghostfolio/common/config';
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
import type {
AssetProfileIdentifier,
BenchmarkMarketDataDetailsResponse,
BenchmarkResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -37,6 +34,7 @@ import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { BenchmarksService } from './benchmarks.service';
import { GetBenchmarkMarketDataDto } from './get-benchmark-market-data.dto';
@Controller('benchmarks')
export class BenchmarksController {
@ -121,38 +119,39 @@ export class BenchmarksController {
@Param('dataSource') dataSource: DataSource,
@Param('startDateString') startDateString: string,
@Param('symbol') symbol: string,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
@Query()
{
accounts,
assetClasses,
dataSource: filterByDataSource,
range,
symbol: filterBySymbol,
tags,
withExcludedAccounts
}: GetBenchmarkMarketDataDto
): Promise<BenchmarkMarketDataDetailsResponse> {
const { endDate, startDate } = getIntervalFromDateRange({
dateRange,
dateRange: range,
startDate: new Date(startDateString)
});
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByTags: tags
});
const withExcludedAccounts = withExcludedAccountsParam === 'true';
return this.benchmarksService.getMarketDataForUser({
dataSource,
dateRange,
endDate,
filters,
impersonationId,
startDate,
symbol,
withExcludedAccounts,
dateRange: range,
user: this.request.user
});
}

12
apps/api/src/app/endpoints/benchmarks/get-benchmark-market-data.dto.ts

@ -0,0 +1,12 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class GetBenchmarkMarketDataDto extends DateRangeFilterDto {
@IsBoolean()
@Transform(({ value }: TransformFnParams) => {
return value === 'true';
})
withExcludedAccounts? = false;
}

12
apps/api/src/app/portfolio/get-details.dto.ts

@ -0,0 +1,12 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class GetDetailsDto extends DateRangeFilterDto {
@IsBoolean()
@Transform(({ value }: TransformFnParams) => {
return value === 'true';
})
withMarkets? = false;
}

10
apps/api/src/app/portfolio/get-dividends.dto.ts

@ -0,0 +1,10 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { GroupBy } from '@ghostfolio/common/types';
import { IsIn, IsOptional } from 'class-validator';
export class GetDividendsDto extends DateRangeFilterDto {
@IsIn(['month', 'year'] as GroupBy[])
@IsOptional()
groupBy?: GroupBy;
}

14
apps/api/src/app/portfolio/get-holdings.dto.ts

@ -0,0 +1,14 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { HoldingType } from '@ghostfolio/common/types';
import { IsIn, IsOptional, IsString } from 'class-validator';
export class GetHoldingsDto extends DateRangeFilterDto {
@IsIn(['ACTIVE', 'CLOSED'] as HoldingType[])
@IsOptional()
holdingType?: HoldingType;
@IsOptional()
@IsString()
query?: string;
}

10
apps/api/src/app/portfolio/get-investments.dto.ts

@ -0,0 +1,10 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { GroupBy } from '@ghostfolio/common/types';
import { IsIn, IsOptional } from 'class-validator';
export class GetInvestmentsDto extends DateRangeFilterDto {
@IsIn(['month', 'year'] as GroupBy[])
@IsOptional()
groupBy?: GroupBy;
}

12
apps/api/src/app/portfolio/get-performance.dto.ts

@ -0,0 +1,12 @@
import { DateRangeFilterDto } from '@ghostfolio/api/dtos/date-range-filter.dto';
import { Transform, TransformFnParams } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class GetPerformanceDto extends DateRangeFilterDto {
@IsBoolean()
@Transform(({ value }: TransformFnParams) => {
return value === 'true';
})
withExcludedAccounts? = false;
}

159
apps/api/src/app/portfolio/portfolio.controller.ts

@ -15,7 +15,6 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import {
DEFAULT_DATE_RANGE,
HEADER_KEY_IMPERSONATION,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
@ -34,11 +33,7 @@ import {
isRestrictedView,
permissions
} from '@ghostfolio/common/permissions';
import type {
DateRange,
GroupBy,
RequestWithUser
} from '@ghostfolio/common/types';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Body,
@ -60,6 +55,11 @@ import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { GetDetailsDto } from './get-details.dto';
import { GetDividendsDto } from './get-dividends.dto';
import { GetHoldingsDto } from './get-holdings.dto';
import { GetInvestmentsDto } from './get-investments.dto';
import { GetPerformanceDto } from './get-performance.dto';
import { PortfolioService } from './portfolio.service';
import { UpdateHoldingTagsDto } from './update-holding-tags.dto';
@ -82,16 +82,17 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withMarkets') withMarketsParam = 'false'
@Query()
{
accounts: filterByAccounts,
assetClasses: filterByAssetClasses,
dataSource: filterByDataSource,
range,
symbol: filterBySymbol,
tags: filterByTags,
withMarkets
}: GetDetailsDto
): Promise<PortfolioDetails & { hasError: boolean }> {
const withMarkets = withMarketsParam === 'true';
let hasDetails = true;
let hasError = false;
@ -118,10 +119,10 @@ export class PortfolioController {
platforms,
summary
} = await this.portfolioService.getDetails({
dateRange,
filters,
impersonationId,
withMarkets,
dateRange: range,
userId: this.request.user.id,
withSummary: true
});
@ -326,20 +327,23 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getDividends(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
@Query()
{
accounts,
assetClasses,
dataSource,
groupBy,
range,
symbol,
tags
}: GetDividendsDto
): Promise<PortfolioDividendsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByDataSource: dataSource,
filterBySymbol: symbol,
filterByTags: tags
});
const impersonationUserId =
@ -349,7 +353,9 @@ export class PortfolioController {
const { settings } = await this.userService.user({ id: userId });
const userCurrency = settings.settings.baseCurrency;
const { endDate, startDate } = getIntervalFromDateRange({ dateRange });
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: range
});
const { activities } = await this.activitiesService.getActivities({
endDate,
@ -429,29 +435,32 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getHoldings(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('holdingType') filterByHoldingType?: string,
@Query('query') filterBySearchQuery?: string,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
@Query()
{
accounts,
assetClasses,
dataSource,
holdingType,
query,
range,
symbol,
tags
}: GetHoldingsDto
): Promise<PortfolioHoldingsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterByHoldingType,
filterBySearchQuery,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByDataSource: dataSource,
filterByHoldingType: holdingType,
filterBySearchQuery: query,
filterBySymbol: symbol,
filterByTags: tags
});
const holdings = await this.portfolioService.getHoldings({
dateRange,
filters,
impersonationId,
dateRange: range,
userId: this.request.user.id
});
@ -463,27 +472,30 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInRequestInterceptor)
public async getInvestments(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string
@Query()
{
accounts,
assetClasses,
dataSource,
groupBy,
range,
symbol,
tags
}: GetInvestmentsDto
): Promise<PortfolioInvestmentsResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByDataSource: dataSource,
filterBySymbol: symbol,
filterByTags: tags
});
let { investments, streaks } = await this.portfolioService.getInvestments({
dateRange,
filters,
groupBy,
impersonationId,
dateRange: range,
savingsRate: this.request.user?.settings?.settings.savingsRate,
userId: this.request.user.id
});
@ -536,29 +548,30 @@ export class PortfolioController {
@Version('2')
public async getPerformanceV2(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('range') dateRange: DateRange = DEFAULT_DATE_RANGE,
@Query('symbol') filterBySymbol?: string,
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccountsParam = 'false'
@Query()
{
accounts,
assetClasses,
dataSource,
range,
symbol,
tags,
withExcludedAccounts
}: GetPerformanceDto
): Promise<PortfolioPerformanceResponse> {
const withExcludedAccounts = withExcludedAccountsParam === 'true';
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByDataSource,
filterBySymbol,
filterByTags
filterByAccounts: accounts,
filterByAssetClasses: assetClasses,
filterByDataSource: dataSource,
filterBySymbol: symbol,
filterByTags: tags
});
const performanceInformation = await this.portfolioService.getPerformance({
dateRange,
filters,
impersonationId,
withExcludedAccounts,
dateRange: range,
userId: this.request.user.id
});

16
apps/api/src/dtos/date-range-filter.dto.ts

@ -0,0 +1,16 @@
import { DATE_RANGES, DEFAULT_DATE_RANGE } from '@ghostfolio/common/config';
import { DateRange } from '@ghostfolio/common/types';
import { Matches } from 'class-validator';
import { FilterDto } from './filter.dto';
// A named date range or a calendar year like '2024', '2023', '2022', etc.
export const DATE_RANGE_PATTERN = new RegExp(
`^(${DATE_RANGES.join('|')}|\\d{4})$`
);
export class DateRangeFilterDto extends FilterDto {
@Matches(DATE_RANGE_PATTERN)
range?: DateRange = DEFAULT_DATE_RANGE;
}

23
apps/api/src/dtos/filter.dto.ts

@ -0,0 +1,23 @@
import { IsOptional, IsString } from 'class-validator';
export class FilterDto {
@IsOptional()
@IsString()
accounts?: string;
@IsOptional()
@IsString()
assetClasses?: string;
@IsOptional()
@IsString()
dataSource?: string;
@IsOptional()
@IsString()
symbol?: string;
@IsOptional()
@IsString()
tags?: string;
}

3
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -341,8 +341,7 @@ export class GfAccountDetailDialogComponent implements OnInit {
}
],
range: DEFAULT_DATE_RANGE,
withExcludedAccounts: true,
withItems: true
withExcludedAccounts: true
})
.pipe(takeUntilDestroyed(this.destroyRef))
}).subscribe({

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

@ -731,13 +731,11 @@ export class DataService {
public fetchPortfolioPerformance({
filters,
range,
withExcludedAccounts = false,
withItems = false
withExcludedAccounts = false
}: {
filters?: Filter[];
range: DateRange;
withExcludedAccounts?: boolean;
withItems?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
@ -746,10 +744,6 @@ export class DataService {
params = params.append('withExcludedAccounts', withExcludedAccounts);
}
if (withItems) {
params = params.append('withItems', withItems);
}
return this.http
.get<any>(`/api/v2/portfolio/performance`, {
params

Loading…
Cancel
Save