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 8 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. 5
      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(
@Param('symbol') symbol: string
): Promise<DataProviderGhostfolioAssetProfileResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const assetProfile = await this.ghostfolioService.getAssetProfile({
@ -92,16 +83,7 @@ export class GhostfolioController {
@Param('symbol') symbol: string,
@Query() { from, granularity, to }: GetDividendsDto
): Promise<DividendsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const dividends = await this.ghostfolioService.getDividends({
@ -132,16 +114,7 @@ export class GhostfolioController {
@Param('symbol') symbol: string,
@Query() { from, granularity, to }: GetHistoricalDto
): Promise<HistoricalResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const historicalData = await this.ghostfolioService.getHistorical({
@ -173,16 +146,7 @@ export class GhostfolioController {
@Query('query') query = ''
): Promise<LookupResponse> {
const includeIndices = includeIndicesParam === 'true';
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const result = await this.ghostfolioService.lookup({
@ -212,16 +176,7 @@ export class GhostfolioController {
@Query('includeHistoricalData', new ParseIntPipe({ optional: true }))
includeHistoricalData = 0
): Promise<MarketDataOfMarketsResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const marketDataOfMarkets =
@ -249,16 +204,7 @@ export class GhostfolioController {
public async getQuotes(
@Query() { symbols }: GetQuotesDto
): Promise<QuotesResponse> {
const maxDailyRequests = await this.ghostfolioService.getMaxDailyRequests();
if (
this.request.user.dataProviderGhostfolioDailyRequests >= maxDailyRequests
) {
throw new HttpException(
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
StatusCodes.TOO_MANY_REQUESTS
);
}
await this.validateDailyRequestLimit();
try {
const quotes = await this.ghostfolioService.getQuotes({
@ -285,4 +231,17 @@ export class GhostfolioController {
public async getStatus(): Promise<DataProviderGhostfolioStatusResponse> {
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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD,
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_SETUP_PERIOD_MAX_REQUESTS_FACTOR,
DEFAULT_CURRENCY,
DERIVED_CURRENCIES
DERIVED_CURRENCIES,
PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS
} from '@ghostfolio/common/config';
import { PROPERTY_DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER_MAX_REQUESTS } from '@ghostfolio/common/config';
import {
getAssetProfileIdentifier,
isValidSearchQuery
@ -37,6 +39,7 @@ import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client';
import { Big } from 'big.js';
import { addMilliseconds, isBefore } from 'date-fns';
import { isEmpty } from 'lodash';
@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) {
const results: QuotesResponse = { quotes: {} };
@ -321,9 +315,17 @@ export class GhostfolioService {
}
public async getStatus({ user }: { user: UserWithSettings }) {
const dailyRequestsMax = await this.getMaxDailyRequests();
return {
dailyRequests: user.dataProviderGhostfolioDailyRequests,
dailyRequestsMax: await this.getMaxDailyRequests(),
dailyRequestsMax,
// 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
};
}
@ -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({
includeIndices = false,
query
@ -420,4 +444,29 @@ export class GhostfolioService {
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;
});
const { createdAt: subscribedAt } = subscriptions.reduce((a, b) => {
return new Date(a.createdAt) < new Date(b.createdAt) ? a : b;
});
let offerKey: SubscriptionOfferKey = price ? 'renewal' : 'default';
if (isBefore(createdAt, parseDate('2023-01-01'))) {
@ -208,6 +212,7 @@ export class SubscriptionService {
return {
offer,
subscribedAt,
expiresAt: isBefore(new Date(), expiresAt) ? expiresAt : undefined,
type: isBefore(new Date(), expiresAt)
? SubscriptionType.Premium

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

@ -147,6 +147,10 @@
isGhostfolioDataProvider(element) &&
isGhostfolioApiKeyValid === true
) {
<div
[matTooltip]="ghostfolioApiStatusTooltip"
[matTooltipDisabled]="!ghostfolioApiStatus.isWithinSetupPeriod"
>
<mat-progress-bar
mode="determinate"
[value]="
@ -162,6 +166,7 @@
{{ ghostfolioApiStatus.dailyRequestsMax }}
<ng-container i18n>daily requests</ng-container>
</small>
</div>
}
</td>
</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 { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
@ -61,6 +62,7 @@ import { catchError, filter, of } from 'rxjs';
MatProgressBarModule,
MatSortModule,
MatTableModule,
MatTooltipModule,
NgxSkeletonLoaderModule,
RouterModule
],
@ -81,6 +83,7 @@ export class GfAdminSettingsComponent implements OnInit {
'actions'
];
public ghostfolioApiStatus: DataProviderGhostfolioStatusResponse;
public readonly ghostfolioApiStatusTooltip = $localize`Additional requests are granted while you are setting up your instance`;
public hasGhostfolioApiKey: boolean;
public isGhostfolioApiKeyValid: boolean;
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
);
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',
* '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 {
dailyRequests: number;
dailyRequestsMax: number;
isWithinSetupPeriod: boolean;
subscription: UserWithSettings['subscription'];
}

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

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

Loading…
Cancel
Save