Browse Source

Task/increase daily request limit of Ghostfolio data provider within setup period (#7674)

Increase daily request limit within setup period
pull/7675/head
Thomas Kaul 9 hours ago
committed by GitHub
parent
commit
45a20d3ea3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 79
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts
  2. 75
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  3. 5
      apps/api/src/app/subscription/subscription.service.ts
  4. 35
      apps/client/src/app/components/admin-settings/admin-settings.component.html
  5. 3
      apps/client/src/app/components/admin-settings/admin-settings.component.ts
  6. 3
      libs/common/src/lib/config.ts
  7. 1
      libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts
  8. 1
      libs/common/src/lib/types/user-with-settings.type.ts

79
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.controller.ts

@ -48,16 +48,7 @@ export class GhostfolioController {
public async getAssetProfile( public async getAssetProfile(
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<DataProviderGhostfolioAssetProfileResponse> { ): Promise<DataProviderGhostfolioAssetProfileResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const assetProfile = await this.ghostfolioService.getAssetProfile({ const assetProfile = await this.ghostfolioService.getAssetProfile({
@ -92,16 +83,7 @@ export class GhostfolioController {
@Param('symbol') symbol: string, @Param('symbol') symbol: string,
@Query() { from, granularity, to }: GetDividendsDto @Query() { from, granularity, to }: GetDividendsDto
): Promise<DividendsResponse> { ): Promise<DividendsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const dividends = await this.ghostfolioService.getDividends({ const dividends = await this.ghostfolioService.getDividends({
@ -132,16 +114,7 @@ export class GhostfolioController {
@Param('symbol') symbol: string, @Param('symbol') symbol: string,
@Query() { from, granularity, to }: GetHistoricalDto @Query() { from, granularity, to }: GetHistoricalDto
): Promise<HistoricalResponse> { ): Promise<HistoricalResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const historicalData = await this.ghostfolioService.getHistorical({ const historicalData = await this.ghostfolioService.getHistorical({
@ -173,16 +146,7 @@ export class GhostfolioController {
@Query('query') query = '' @Query('query') query = ''
): Promise<LookupResponse> { ): Promise<LookupResponse> {
const includeIndices = includeIndicesParam === 'true'; const includeIndices = includeIndicesParam === 'true';
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const result = await this.ghostfolioService.lookup({ const result = await this.ghostfolioService.lookup({
@ -212,16 +176,7 @@ export class GhostfolioController {
@Query('includeHistoricalData', new ParseIntPipe({ optional: true })) @Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0 includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> { ): Promise<MarketDataOfMarketsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const marketDataOfMarkets = const marketDataOfMarkets =
@ -249,16 +204,7 @@ export class GhostfolioController {
public async getQuotes( public async getQuotes(
@Query() { symbols }: GetQuotesDto @Query() { symbols }: GetQuotesDto
): Promise<QuotesResponse> { ): Promise<QuotesResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests(); await this.validateDailyRequestLimit();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
try { try {
const quotes = await this.ghostfolioService.getQuotes({ const quotes = await this.ghostfolioService.getQuotes({
@ -285,4 +231,17 @@ export class GhostfolioController {
public async getStatus(): Promise<DataProviderGhostfolioStatusResponse> { public async getStatus(): Promise<DataProviderGhostfolioStatusResponse> {
return this.ghostfolioService.getStatus({ user: this.request.user }); return this.ghostfolioService.getStatus({ user: this.request.user });
} }
private async validateDailyRequestLimit() {
if (
await this.ghostfolioService.isDailyRequestLimitExceeded({
user: this.request.user
})
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
}
} }

75
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts

@ -13,10 +13,12 @@ import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD,
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD_MAX_REQUESTS_FACTOR,
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DERIVED_CURRENCIES DERIVED_CURRENCIES,
PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import { import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
isValidSearchQuery isValidSearchQuery
@ -37,6 +39,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addMilliseconds, isBefore } from 'date-fns';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@Injectable() @Injectable()
@ -232,15 +235,6 @@ export class GhostfolioService {
} }
} }
public async getMaxDailyRequests() {
return parseInt(
(await this.propertyService.getByKey<string>(
PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS
)) || '0',
10
);
}
public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) { public async getQuotes({ requestTimeout, symbols }: GetQuotesParams) {
const results: QuotesResponse = { quotes: {} }; const results: QuotesResponse = { quotes: {} };
@ -321,9 +315,17 @@ export class GhostfolioService {
} }
public async getStatus({ user }: { user: UserWithSettings }) { public async getStatus({ user }: { user: UserWithSettings }) {
const dailyRequestsMax = await this.getMaxDailyRequests();
return { return {
dailyRequests: user.dataProviderGhostfolioDailyRequests, dailyRequestsMax,
dailyRequestsMax: await this.getMaxDailyRequests(), // Cap the reported requests, as they can exceed the reported limit
// within the setup period
dailyRequests: Math.min(
dailyRequestsMax,
user.dataProviderGhostfolioDailyRequests
),
isWithinSetupPeriod: this.isWithinSetupPeriod({ user }),
subscription: user.subscription subscription: user.subscription
}; };
} }
@ -341,6 +343,28 @@ export class GhostfolioService {
}); });
} }
public async isDailyRequestLimitExceeded({
user
}: {
user: UserWithSettings;
}) {
const maxDailyRequests = await this.getMaxDailyRequests();
if (user.dataProviderGhostfolioDailyRequests < maxDailyRequests) {
return false;
}
if (this.isWithinSetupPeriod({ user })) {
return (
user.dataProviderGhostfolioDailyRequests >=
maxDailyRequests *
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD_MAX_REQUESTS_FACTOR
);
}
return true;
}
public async lookup({ public async lookup({
includeIndices = false, includeIndices = false,
query query
@ -420,4 +444,29 @@ export class GhostfolioService {
return this.dataProviderService.getDataProvider(DataSource[dataSource]); return this.dataProviderService.getDataProvider(DataSource[dataSource]);
}); });
} }
private async getMaxDailyRequests() {
return parseInt(
(await this.propertyService.getByKey<string>(
PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS
)) || '0',
10
);
}
private isWithinSetupPeriod({ user }: { user: UserWithSettings }) {
const subscribedAt = user.subscription?.subscribedAt;
if (!subscribedAt) {
return false;
}
return isBefore(
new Date(),
addMilliseconds(
subscribedAt,
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD
)
);
}
} }

5
apps/api/src/app/subscription/subscription.service.ts

@ -192,6 +192,10 @@ export class SubscriptionService {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
}); });
const { createdAt: subscribedAt } = subscriptions.reduce((a, b) => {
return new Date(a.createdAt) < new Date(b.createdAt) ? a : b;
});
let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default'; let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) { if (isBefore(createdAt, parseDate('2023-01-01'))) {
@ -208,6 +212,7 @@ export class SubscriptionService {
return { return {
offer, offer,
subscribedAt,
expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined, expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined,
type: isBefore(new Date(), expiresAt) type: isBefore(new Date(), expiresAt)
? SubscriptionType.Premium ? SubscriptionType.Premium

35
apps/client/src/app/components/admin-settings/admin-settings.component.html

@ -147,21 +147,26 @@
isGhostfolioDataProvider(element) && isGhostfolioDataProvider(element) &&
isGhostfolioApiKeyValid === true isGhostfolioApiKeyValid === true
) { ) {
<mat-progress-bar <div
mode="determinate" [matTooltip]="ghostfolioApiStatusTooltip"
[value]=" [matTooltipDisabled]="!ghostfolioApiStatus.isWithinSetupPeriod"
100 - >
(ghostfolioApiStatus.dailyRequests / <mat-progress-bar
ghostfolioApiStatus.dailyRequestsMax) * mode="determinate"
100 [value]="
" 100 -
/> (ghostfolioApiStatus.dailyRequests /
<small class="text-muted"> ghostfolioApiStatus.dailyRequestsMax) *
{{ ghostfolioApiStatus.dailyRequests }} 100
<ng-container i18n>of</ng-container> "
{{ ghostfolioApiStatus.dailyRequestsMax }} />
<ng-container i18n>daily requests</ng-container> <small class="text-muted">
</small> {{ ghostfolioApiStatus.dailyRequests }}
<ng-container i18n>of</ng-container>
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
</small>
</div>
} }
</td> </td>
</ng-container> </ng-container>

3
apps/client/src/app/components/admin-settings/admin-settings.component.ts

@ -36,6 +36,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
@ -61,6 +62,7 @@ import { catchError, filter, of } from 'rxjs';
MatProgressBarModule, MatProgressBarModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatTooltipModule,
NgxSkeletonLoaderModule, NgxSkeletonLoaderModule,
RouterModule RouterModule
], ],
@ -81,6 +83,7 @@ export class GfAdminSettingsComponent implements OnInit {
'actions' 'actions'
]; ];
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse; public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public readonly ghostfolioApiStatusTooltip = $localize`Additional requests are granted while you are setting up your instance`;
public hasGhostfolioApiKey: boolean; public hasGhostfolioApiKey: boolean;
public isGhostfolioApiKeyValid: boolean; public isGhostfolioApiKeyValid: boolean;
public isLoading = false; public isLoading = false;

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

@ -67,6 +67,9 @@ export const DATA_GATHERING_QUEUE_PRIORITY_MEDIUM = Math.round(
DATA_GATHERING_QUEUE_PRIORITY_LOW / 2 DATA_GATHERING_QUEUE_PRIORITY_LOW / 2
); );
export const DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD = ms('2 weeks');
export const DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD_MAX_REQUESTS_FACTOR = 2;
/** /**
* The named date ranges, complemented by the calendar years like '2024', * The named date ranges, complemented by the calendar years like '2024',
* '2023', '2022', etc. * '2023', '2022', etc.

1
libs/common/src/lib/interfaces/responses/data-provider-ghostfolio-status-response.interface.ts

@ -3,5 +3,6 @@ import { UserWithSettings } from '@ghostfolio/common/types';
export interface DataProviderGhostfolioStatusResponse { export interface DataProviderGhostfolioStatusResponse {
dailyRequests: number; dailyRequests: number;
dailyRequestsMax: number; dailyRequestsMax: number;
isWithinSetupPeriod: boolean;
subscription: UserWithSettings['subscription']; subscription: UserWithSettings['subscription'];
} }

1
libs/common/src/lib/types/user-with-settings.type.ts

@ -13,6 +13,7 @@ export type UserWithSettings = User & {
subscription?: { subscription?: {
expiresAt?: Date; expiresAt?: Date;
offer: SubscriptionOffer; offer: SubscriptionOffer;
subscribedAt?: Date;
type: SubscriptionType; type: SubscriptionType;
}; };
}; };

Loading…
Cancel
Save