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
- 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`
### 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 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

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 { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.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 { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import {
ActivitiesResponse,
ActivityResponse
ActivityResponse,
CreateActivityResponse
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { scopes } from '@ghostfolio/common/scopes';
@ -33,7 +35,7 @@ import {
Query,
UseInterceptors
} from '@nestjs/common';
import { Order, Prisma } from '@prisma/client';
import { Order } from '@prisma/client';
import { parseISO } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -47,7 +49,8 @@ export class ActivitiesController {
private readonly activitiesService: ActivitiesService,
private readonly apiService: ApiService,
private readonly dataProviderService: DataProviderService,
private readonly dataGatheringService: DataGatheringService
private readonly dataGatheringService: DataGatheringService,
private readonly symbolProfileService: SymbolProfileService
) {}
@Delete()
@ -209,6 +212,7 @@ export class ActivitiesController {
@RequiresScope(scopes.activityCreate)
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async createActivity(
@Body() data: CreateOrderDto,
@Impersonation()
@ -217,7 +221,7 @@ export class ActivitiesController {
userId,
userSubscription
}: ImpersonationContext
): Promise<Prisma.OrderGetPayload<{ include: { SymbolProfile: true } }>> {
): Promise<CreateActivityResponse> {
// Evaluate the more restrictive subscription of the authenticated user
// and the owner of the activity
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)

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

@ -12,9 +12,10 @@ import {
ResponseError
} from '@ghostfolio/common/interfaces';
import { utc } from '@date-fns/utc';
import { Injectable } from '@nestjs/common';
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 { GetValueObject } from './interfaces/get-value-object.interface';
@ -38,14 +39,14 @@ export class CurrentRateService {
subscriptionType
}: GetValuesParams): Promise<GetValuesObject> {
const dataProviderInfos: DataProviderInfo[] = [];
const today = resetHours(new Date());
const includesToday =
(!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) &&
(!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 today = resetHours(new Date());
const values: GetValueObject[] = [];
if (includesToday) {
@ -128,7 +129,7 @@ export class CurrentRateService {
return (
currentValue.dataSource === dataSource &&
currentValue.symbol === symbol &&
isToday(currentValue.date)
isSameDay(currentValue.date, today, { in: utc })
);
});
@ -190,12 +191,15 @@ export class CurrentRateService {
return response;
}
private containsToday(dates: Date[]): boolean {
for (const date of dates) {
if (isToday(date)) {
return true;
}
}
return false;
private containsToday({
dates,
today
}: {
dates: Date[];
today: Date;
}): 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
// relation to the symbol profile
it('should redact an activity with the relation to the symbol profile', () => {
// asset profile and the deprecated relation to the symbol profile
it('should redact an activity with the asset profile and the relation to the symbol profile', () => {
expect(
redactPaths({
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',
currency: 'USD',
fee: 19.9,
@ -3149,6 +3162,14 @@ describe('redactAttributes', () => {
paths: DEFAULT_REDACTED_PATHS
})
).toStrictEqual({
assetProfile: {
comment: null,
name: 'Apple Inc',
scraperConfiguration: null,
symbol: 'AAPL',
symbolMapping: null,
watchedByCount: null
},
comment: null,
currency: 'USD',
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',
'latestActivities[*].assetProfile.dataSource',
'settings["filters.dataSource"]',
/* @deprecated */
'SymbolProfile.dataSource',
'watchlist[*].dataSource'
]
});

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

@ -342,7 +342,7 @@
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">
<div class="flex-grow-1 mr-2 text-truncate">
{{ holding.assetProfile.name }}
</div>
<div class="d-flex justify-content-end">
@ -393,7 +393,7 @@
}"
[routerLink]="[]"
>
<div class="flex-grow-1 mr-2">
<div class="flex-grow-1 mr-2 text-truncate">
{{ holding.assetProfile.name }}
</div>
<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.projectedTotalAmount',
'settings.savingsRate',
/* @deprecated */
'SymbolProfile.comment',
/* @deprecated */
'SymbolProfile.scraperConfiguration',
/* @deprecated */
'SymbolProfile.symbolMapping',
'totalBalanceInBaseCurrency',
'totalDividendInBaseCurrency',
'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 { BenchmarkMarketDataDetailsResponse } from './responses/benchmark-market-data-details-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 { DataEnhancerHealthResponse } from './responses/data-enhancer-health-response.interface';
import type { DataProviderGhostfolioAssetProfileResponse } from './responses/data-provider-ghostfolio-asset-profile-response.interface';
@ -133,6 +134,7 @@ export {
BenchmarkProperty,
BenchmarkResponse,
Coupon,
CreateActivityResponse,
CreateStripeCheckoutSessionResponse,
DataEnhancerHealthResponse,
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 =
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 currentBalance = input<number | null>();
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 sort = viewChild(MatSort);

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

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

Loading…
Cancel
Save