Browse Source

Merge branch 'main' into task/improve-performance-of-latest-market-data

pull/8059/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
240eba05e9
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      CHANGELOG.md
  2. 21
      apps/api/src/app/activities/activities.controller.ts
  3. 26
      apps/api/src/app/portfolio/current-rate.service.ts
  4. 25
      apps/api/src/helper/object.helper.spec.ts
  5. 4
      apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts
  6. 4
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  7. 7
      libs/common/src/lib/config.ts
  8. 2
      libs/common/src/lib/interfaces/index.ts
  9. 12
      libs/common/src/lib/interfaces/responses/create-activity-response.interface.ts
  10. 11
      libs/ui/src/lib/account-balances/account-balances.component.ts
  11. 3
      libs/ui/src/lib/services/data.service.ts

3
CHANGELOG.md

@ -14,12 +14,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the performance of getting the latest market data by reordering the columns of the unique constraint in the market data table - Improved the performance of getting the latest market data by reordering the columns of the unique constraint in the market data table
- Deprecated `SymbolProfile` in favor of `assetProfile` in the endpoint `POST api/v1/activities`
- Upgraded `Nx` from version `23.1.1` to `23.2.1` - Upgraded `Nx` from version `23.1.1` to `23.2.1`
### Fixed ### Fixed
- Fixed a layout issue in the top and bottom holdings of the analysis page by truncating long names
- Fixed the missing thousands separator of 4-digit numbers in certain locales - Fixed the missing thousands separator of 4-digit numbers in certain locales
- Fixed an issue where holdings without a market price have been valued at the unit price of a dividend, a fee, an interest or a liability - Fixed an issue where holdings without a market price have been valued at the unit price of a dividend, a fee, an interest or a liability
- Fixed the check for today in the current rate service for instances running in a time zone behind UTC
## 3.79.0 - 2026-10-04 ## 3.79.0 - 2026-10-04

21
apps/api/src/app/activities/activities.controller.ts

@ -9,13 +9,15 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { ApiService } from '@ghostfolio/api/services/api/api.service'; import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums'; import { SubscriptionType } from '@ghostfolio/common/enums';
import { import {
ActivitiesResponse, ActivitiesResponse,
ActivityResponse ActivityResponse,
CreateActivityResponse
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes'; import { scopes } from '@ghostfolio/common/scopes';
@ -33,7 +35,7 @@ import {
Query, Query,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { Order, Prisma } from '@prisma/client'; import { Order } from '@prisma/client';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -47,7 +49,8 @@ export class ActivitiesController {
private readonly activitiesService: ActivitiesService, private readonly activitiesService: ActivitiesService,
private readonly apiService: ApiService, private readonly apiService: ApiService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly dataGatheringService: DataGatheringService private readonly dataGatheringService: DataGatheringService,
private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@Delete() @Delete()
@ -209,6 +212,7 @@ export class ActivitiesController {
@RequiresScope(scopes.activityCreate) @RequiresScope(scopes.activityCreate)
@UseInterceptors(RedactValuesInResponseInterceptor) @UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor) @UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async createActivity( public async createActivity(
@Body() data: CreateOrderDto, @Body() data: CreateOrderDto,
@Impersonation() @Impersonation()
@ -217,7 +221,7 @@ export class ActivitiesController {
userId, userId,
userSubscription userSubscription
}: ImpersonationContext }: ImpersonationContext
): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> { ): Promise<CreateActivityResponse> {
// Evaluate the more restrictive subscription of the authenticated user // Evaluate the more restrictive subscription of the authenticated user
// and the owner of the activity // and the owner of the activity
const subscription = const subscription =
@ -303,7 +307,14 @@ export class ActivitiesController {
}); });
} }
return activity; const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([
{
dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol
}
]);
return { ...activity, assetProfile };
} }
@HasPermission(permissions.updateActivity) @HasPermission(permissions.updateActivity)

26
apps/api/src/app/portfolio/current-rate.service.ts

@ -12,9 +12,10 @@ import {
ResponseError ResponseError
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { utc } from '@date-fns/utc';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Type as ActivityType } from '@prisma/client'; import { Type as ActivityType } from '@prisma/client';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isSameDay } from 'date-fns';
import { isEmpty, uniqBy } from 'lodash-es'; import { isEmpty, uniqBy } from 'lodash-es';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
@ -38,14 +39,14 @@ export class CurrentRateService {
subscriptionType subscriptionType
}: GetValuesParams): Promise<GetValuesObject> { }: GetValuesParams): Promise<GetValuesObject> {
const dataProviderInfos: DataProviderInfo[] = []; const dataProviderInfos: DataProviderInfo[] = [];
const today = resetHours(new Date());
const includesToday = const includesToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
(!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) &&
(!dateQuery.in || this.containsToday(dateQuery.in)); (!dateQuery.in || this.containsToday({ today, dates: dateQuery.in }));
const quoteErrors: ResponseError['errors'] = []; const quoteErrors: ResponseError['errors'] = [];
const today = resetHours(new Date());
const values: GetValueObject[] = []; const values: GetValueObject[] = [];
if (includesToday) { if (includesToday) {
@ -128,7 +129,7 @@ export class CurrentRateService {
return ( return (
currentValue.dataSource === dataSource && currentValue.dataSource === dataSource &&
currentValue.symbol === symbol && currentValue.symbol === symbol &&
isToday(currentValue.date) isSameDay(currentValue.date, today, { in: utc })
); );
}); });
@ -190,12 +191,15 @@ export class CurrentRateService {
return response; return response;
} }
private containsToday(dates: Date[]): boolean { private containsToday({
for (const date of dates) { dates,
if (isToday(date)) { today
return true; }: {
} dates: Date[];
} today: Date;
return false; }): boolean {
return dates.some((date) => {
return isSameDay(date, today, { in: utc });
});
} }
} }

25
apps/api/src/helper/object.helper.spec.ts

@ -3121,11 +3121,24 @@ describe('redactAttributes', () => {
}); });
// The endpoint POST api/v1/activities returns a row of the database with the // The endpoint POST api/v1/activities returns a row of the database with the
// relation to the symbol profile // asset profile and the deprecated relation to the symbol profile
it('should redact an activity with the relation to the symbol profile', () => { it('should redact an activity with the asset profile and the relation to the symbol profile', () => {
expect( expect(
redactPaths({ redactPaths({
object: { object: {
assetProfile: {
comment: 'Reviewed by the administrator',
name: 'Apple Inc',
scraperConfiguration: {
headers: { Authorization: 'Bearer TOKEN' },
mode: 'lazy',
selector: '.price',
url: 'https://example.org/get_price'
},
symbol: 'AAPL',
symbolMapping: { YAHOO: 'AAPL' },
watchedByCount: 7
},
comment: 'Bought on a dip', comment: 'Bought on a dip',
currency: 'USD', currency: 'USD',
fee: 19.9, fee: 19.9,
@ -3149,6 +3162,14 @@ describe('redactAttributes', () => {
paths: DEFAULT_REDACTED_PATHS paths: DEFAULT_REDACTED_PATHS
}) })
).toStrictEqual({ ).toStrictEqual({
assetProfile: {
comment: null,
name: 'Apple Inc',
scraperConfiguration: null,
symbol: 'AAPL',
symbolMapping: null,
watchedByCount: null
},
comment: null, comment: null,
currency: 'USD', currency: 'USD',
fee: null, fee: null,

4
apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts

@ -94,6 +94,10 @@ export class TransformDataSourceInResponseInterceptor<
'items[*].dataSource', 'items[*].dataSource',
'latestActivities[*].assetProfile.dataSource', 'latestActivities[*].assetProfile.dataSource',
'settings["filters.dataSource"]', 'settings["filters.dataSource"]',
/* @deprecated */
'SymbolProfile.dataSource',
'watchlist[*].dataSource' 'watchlist[*].dataSource'
] ]
}); });

4
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -342,7 +342,7 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2"> <div class="flex-grow-1 mr-2 text-truncate">
{{ holding.assetProfile.name }} {{ holding.assetProfile.name }}
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -393,7 +393,7 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2"> <div class="flex-grow-1 mr-2 text-truncate">
{{ holding.assetProfile.name }} {{ holding.assetProfile.name }}
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">

7
libs/common/src/lib/config.ts

@ -180,9 +180,16 @@ export const DEFAULT_REDACTED_PATHS = [
'settings.emergencyFund', 'settings.emergencyFund',
'settings.projectedTotalAmount', 'settings.projectedTotalAmount',
'settings.savingsRate', 'settings.savingsRate',
/* @deprecated */
'SymbolProfile.comment', 'SymbolProfile.comment',
/* @deprecated */
'SymbolProfile.scraperConfiguration', 'SymbolProfile.scraperConfiguration',
/* @deprecated */
'SymbolProfile.symbolMapping', 'SymbolProfile.symbolMapping',
'totalBalanceInBaseCurrency', 'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency', 'totalDividendInBaseCurrency',
'totalInterestInBaseCurrency', 'totalInterestInBaseCurrency',

2
libs/common/src/lib/interfaces/index.ts

@ -53,6 +53,7 @@ import type { AssetProfilesResponse } from './responses/asset-profiles-response.
import type { AssetResponse } from './responses/asset-response.interface'; import type { AssetResponse } from './responses/asset-response.interface';
import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface'; import type { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-response.interface';
import type { BenchmarkResponse } from './responses/benchmark-response.interface'; import type { BenchmarkResponse } from './responses/benchmark-response.interface';
import type { CreateActivityResponse } from './responses/create-activity-response.interface';
import type { CreateStripeCheckoutSessionResponse } from './responses/create-stripe-checkout-session-response.interface'; import type { CreateStripeCheckoutSessionResponse } from './responses/create-stripe-checkout-session-response.interface';
import type { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface'; import type { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface';
import type { DataProviderGhostfolioAssetProfileResponse } from './responses/data-provider-ghostfolio-asset-profile-response.interface'; import type { DataProviderGhostfolioAssetProfileResponse } from './responses/data-provider-ghostfolio-asset-profile-response.interface';
@ -133,6 +134,7 @@ export {
BenchmarkProperty, BenchmarkProperty,
BenchmarkResponse, BenchmarkResponse,
Coupon, Coupon,
CreateActivityResponse,
CreateStripeCheckoutSessionResponse, CreateStripeCheckoutSessionResponse,
DataEnhancerHealthResponse, DataEnhancerHealthResponse,
DataProviderGhostfolioAssetProfileResponse, DataProviderGhostfolioAssetProfileResponse,

12
libs/common/src/lib/interfaces/responses/create-activity-response.interface.ts

@ -0,0 +1,12 @@
import type { Order, SymbolProfile } from '@ghostfolio/prisma/browser';
import { EnhancedAssetProfile } from '../enhanced-asset-profile.interface';
export interface CreateActivityResponse extends Order {
assetProfile: EnhancedAssetProfile;
/**
* @deprecated Use `assetProfile` instead
*/
SymbolProfile: SymbolProfile;
}

11
libs/ui/src/lib/account-balances/account-balances.component.ts

@ -69,11 +69,18 @@ export class GfAccountBalancesComponent implements OnChanges, OnInit {
public readonly accountBalances = public readonly accountBalances =
input.required<AccountBalancesResponse['balances']>(); input.required<AccountBalancesResponse['balances']>();
public readonly accountCurrency = input.required<string>();
public readonly accountCurrency = input.required<string | null>();
public readonly accountId = input.required<string>(); public readonly accountId = input.required<string>();
public readonly currentBalance = input<number | null>(); public readonly currentBalance = input<number | null>();
public readonly displayedColumns: string[] = ['date', 'value', 'actions']; public readonly displayedColumns: string[] = ['date', 'value', 'actions'];
public readonly locale = input(getLocale());
public readonly locale = input(getLocale(), {
transform: (value?: string) => {
return value ?? getLocale();
}
});
public readonly showActions = input(true); public readonly showActions = input(true);
public readonly sort = viewChild(MatSort); public readonly sort = viewChild(MatSort);

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

@ -34,6 +34,7 @@ import {
AssetResponse, AssetResponse,
BenchmarkMarketDataDetailsResponse, BenchmarkMarketDataDetailsResponse,
BenchmarkResponse, BenchmarkResponse,
CreateActivityResponse,
CreateStripeCheckoutSessionResponse, CreateStripeCheckoutSessionResponse,
DataProviderHealthResponse, DataProviderHealthResponse,
DataProviderHistoricalResponse, DataProviderHistoricalResponse,
@ -873,7 +874,7 @@ export class DataService {
} }
public postActivity(aOrder: CreateOrderDto) { public postActivity(aOrder: CreateOrderDto) {
return this.http.post<Order>('/api/v1/activities', aOrder); return this.http.post<CreateActivityResponse>('/api/v1/activities', aOrder);
} }
public postApiKey() { public postApiKey() {

Loading…
Cancel
Save