Browse Source

Task/add HTTP fetch service (#6951)

* Add HTTP fetch service

* Update changelog
pull/6952/head
Thomas Kaul 5 days ago
committed by GitHub
parent
commit
f882e3ddf4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 10
      apps/api/src/app/auth/auth.module.ts
  3. 2
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts
  4. 3
      apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.service.ts
  5. 2
      apps/api/src/app/logo/logo.module.ts
  6. 8
      apps/api/src/app/logo/logo.service.ts
  7. 34
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  8. 3
      apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts
  9. 13
      apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts
  10. 13
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  11. 2
      apps/api/src/services/data-provider/data-provider.module.ts
  12. 35
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  13. 77
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  14. 12
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  15. 4
      apps/api/src/services/data-provider/manual/manual.service.ts
  16. 13
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  17. 9
      apps/api/src/services/fetch/fetch.module.ts
  18. 63
      apps/api/src/services/fetch/fetch.service.ts
  19. 2
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts
  20. 32
      apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts

4
CHANGELOG.md

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added the `FetchService` to centralize outbound HTTP requests
### Changed ### Changed
- Upgraded `nestjs` from version `11.1.19` to `11.1.21` - Upgraded `nestjs` from version `11.1.19` to `11.1.21`

10
apps/api/src/app/auth/auth.module.ts

@ -5,6 +5,8 @@ import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -23,6 +25,7 @@ import { OidcStrategy } from './oidc.strategy';
controllers: [AuthController], controllers: [AuthController],
imports: [ imports: [
ConfigurationModule, ConfigurationModule,
FetchModule,
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' } signOptions: { expiresIn: '180 days' }
@ -40,11 +43,12 @@ import { OidcStrategy } from './oidc.strategy';
GoogleStrategy, GoogleStrategy,
JwtStrategy, JwtStrategy,
{ {
inject: [AuthService, ConfigurationService], inject: [AuthService, ConfigurationService, FetchService],
provide: OidcStrategy, provide: OidcStrategy,
useFactory: async ( useFactory: async (
authService: AuthService, authService: AuthService,
configurationService: ConfigurationService configurationService: ConfigurationService,
fetchService: FetchService
) => { ) => {
const isOidcEnabled = configurationService.get( const isOidcEnabled = configurationService.get(
'ENABLE_FEATURE_AUTH_OIDC' 'ENABLE_FEATURE_AUTH_OIDC'
@ -81,7 +85,7 @@ import { OidcStrategy } from './oidc.strategy';
} else { } else {
// Fetch OIDC configuration from discovery endpoint // Fetch OIDC configuration from discovery endpoint
try { try {
const response = await fetch( const response = await fetchService.fetch(
`${issuer}/.well-known/openid-configuration` `${issuer}/.well-known/openid-configuration`
); );

2
apps/api/src/app/endpoints/data-providers/ghostfolio/ghostfolio.module.ts

@ -12,6 +12,7 @@ import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/goog
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -27,6 +28,7 @@ import { GhostfolioService } from './ghostfolio.service';
imports: [ imports: [
CryptocurrencyModule, CryptocurrencyModule,
DataProviderModule, DataProviderModule,
FetchModule,
MarketDataModule, MarketDataModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,

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

@ -8,6 +8,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
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 {
@ -36,6 +37,7 @@ export class GhostfolioService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService private readonly propertyService: PropertyService
) {} ) {}
@ -355,6 +357,7 @@ export class GhostfolioService {
private getDataProviderInfo(): DataProviderInfo { private getDataProviderInfo(): DataProviderInfo {
const ghostfolioDataProviderService = new GhostfolioDataProviderService( const ghostfolioDataProviderService = new GhostfolioDataProviderService(
this.configurationService, this.configurationService,
this.fetchService,
this.propertyService this.propertyService
); );

2
apps/api/src/app/logo/logo.module.ts

@ -1,5 +1,6 @@
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -11,6 +12,7 @@ import { LogoService } from './logo.service';
controllers: [LogoController], controllers: [LogoController],
imports: [ imports: [
ConfigurationModule, ConfigurationModule,
FetchModule,
SymbolProfileModule, SymbolProfileModule,
TransformDataSourceInRequestModule TransformDataSourceInRequestModule
], ],

8
apps/api/src/app/logo/logo.service.ts

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -10,6 +11,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
export class LogoService { export class LogoService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -43,7 +45,8 @@ export class LogoService {
} }
private async getBuffer(aUrl: string) { private async getBuffer(aUrl: string) {
const blob = await fetch( const blob = await this.fetchService
.fetch(
`https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`, `https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${aUrl}&size=64`,
{ {
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
@ -51,7 +54,8 @@ export class LogoService {
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).then((res) => res.blob()); )
.then((res) => res.blob());
return { return {
buffer: await blob.arrayBuffer().then((arrayBuffer) => { buffer: await blob.arrayBuffer().then((arrayBuffer) => {

34
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -7,6 +7,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { import {
@ -32,7 +33,8 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
private headers: HeadersInit = {}; private headers: HeadersInit = {};
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {} ) {}
public onModuleInit() { public onModuleInit() {
@ -67,12 +69,14 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
}; };
try { try {
const { name } = await fetch(`${this.apiUrl}/coins/${symbol}`, { const { name } = await this.fetchService
.fetch(`${this.apiUrl}/coins/${symbol}`, {
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}).then((res) => res.json()); })
.then((res) => res.json());
response.name = name; response.name = name;
} catch (error) { } catch (error) {
@ -118,13 +122,15 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
vs_currency: DEFAULT_CURRENCY.toLowerCase() vs_currency: DEFAULT_CURRENCY.toLowerCase()
}); });
const { error, prices, status } = await fetch( const { error, prices, status } = await this.fetchService
.fetch(
`${this.apiUrl}/coins/${symbol}/market_chart/range?${queryParams.toString()}`, `${this.apiUrl}/coins/${symbol}/market_chart/range?${queryParams.toString()}`,
{ {
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
if (error?.status) { if (error?.status) {
throw new Error(error.status.error_message); throw new Error(error.status.error_message);
@ -181,13 +187,12 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
vs_currencies: DEFAULT_CURRENCY.toLowerCase() vs_currencies: DEFAULT_CURRENCY.toLowerCase()
}); });
const quotes = await fetch( const quotes = await this.fetchService
`${this.apiUrl}/simple/price?${queryParams.toString()}`, .fetch(`${this.apiUrl}/simple/price?${queryParams.toString()}`, {
{
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json()); .then((res) => res.json());
for (const symbol in quotes) { for (const symbol in quotes) {
response[symbol] = { response[symbol] = {
@ -230,13 +235,12 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
query query
}); });
const { coins } = await fetch( const { coins } = await this.fetchService
`${this.apiUrl}/search?${queryParams.toString()}`, .fetch(`${this.apiUrl}/search?${queryParams.toString()}`, {
{
headers: this.headers, headers: this.headers,
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json()); .then((res) => res.json());
items = coins.map(({ id: symbol, name }) => { items = coins.map(({ id: symbol, name }) => {
return { return {

3
apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts

@ -3,6 +3,7 @@ import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cr
import { OpenFigiDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/openfigi/openfigi.service'; import { OpenFigiDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/openfigi/openfigi.service';
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service'; import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -16,7 +17,7 @@ import { DataEnhancerService } from './data-enhancer.service';
YahooFinanceDataEnhancerService, YahooFinanceDataEnhancerService,
'DataEnhancers' 'DataEnhancers'
], ],
imports: [ConfigurationModule, CryptocurrencyModule], imports: [ConfigurationModule, CryptocurrencyModule, FetchModule],
providers: [ providers: [
DataEnhancerService, DataEnhancerService,
OpenFigiDataEnhancerService, OpenFigiDataEnhancerService,

13
apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts

@ -1,5 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { parseSymbol } from '@ghostfolio/common/helper'; import { parseSymbol } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -10,7 +11,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://api.openfigi.com'; private static baseUrl = 'https://api.openfigi.com';
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {} ) {}
public async enhance({ public async enhance({
@ -42,9 +44,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
this.configurationService.get('API_KEY_OPEN_FIGI'); this.configurationService.get('API_KEY_OPEN_FIGI');
} }
const mappings = (await fetch( const mappings = (await this.fetchService
`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, .fetch(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {
{
body: JSON.stringify([ body: JSON.stringify([
{ exchCode: exchange, idType: 'TICKER', idValue: ticker } { exchCode: exchange, idType: 'TICKER', idValue: ticker }
]), ]),
@ -54,8 +55,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
}, },
method: 'POST', method: 'POST',
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json())) as any[]; .then((res) => res.json())) as any[];
if (mappings?.length === 1 && mappings[0].data?.length === 1) { if (mappings?.length === 1 && mappings[0].data?.length === 1) {
const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0]; const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0];

13
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -1,5 +1,6 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { Holding } from '@ghostfolio/common/interfaces'; import { Holding } from '@ghostfolio/common/interfaces';
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
@ -23,7 +24,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}; };
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {} ) {}
public async enhance({ public async enhance({
@ -60,7 +62,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const profile = await fetch( const profile = await this.fetchService
.fetch(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${trackinsightSymbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/funds/${trackinsightSymbol}.json`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
@ -83,7 +86,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.isin = isin; response.isin = isin;
} }
const holdings = await fetch( const holdings = await this.fetchService
.fetch(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${trackinsightSymbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/holdings/${trackinsightSymbol}.json`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
@ -182,7 +186,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
requestTimeout: number; requestTimeout: number;
symbol: string; symbol: string;
}) { }) {
return fetch( return this.fetchService
.fetch(
`https://www.trackinsight.com/search-api/search_v2/${symbol}/_/ticker/default/0/3`, `https://www.trackinsight.com/search-api/search_v2/${symbol}/_/ticker/default/0/3`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)

2
apps/api/src/services/data-provider/data-provider.module.ts

@ -10,6 +10,7 @@ import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/goog
import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service';
import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -26,6 +27,7 @@ import { DataProviderService } from './data-provider.service';
ConfigurationModule, ConfigurationModule,
CryptocurrencyModule, CryptocurrencyModule,
DataEnhancerModule, DataEnhancerModule,
FetchModule,
MarketDataModule, MarketDataModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,

35
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -7,6 +7,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -41,6 +42,7 @@ export class EodHistoricalDataService
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -111,12 +113,11 @@ export class EodHistoricalDataService
[date: string]: DataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const historicalResult = await fetch( const historicalResult = await this.fetchService
`${this.URL}/div/${symbol}?${queryParams.toString()}`, .fetch(`${this.URL}/div/${symbol}?${queryParams.toString()}`, {
{
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json()); .then((res) => res.json());
for (const { date, value } of historicalResult) { for (const { date, value } of historicalResult) {
response[date] = { response[date] = {
@ -158,12 +159,11 @@ export class EodHistoricalDataService
to: format(to, DATE_FORMAT) to: format(to, DATE_FORMAT)
}); });
const response = await fetch( const response = await this.fetchService
`${this.URL}/eod/${symbol}?${queryParams.toString()}`, .fetch(`${this.URL}/eod/${symbol}?${queryParams.toString()}`, {
{
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json()); .then((res) => res.json());
return response.reduce( return response.reduce(
(result, { adjusted_close, date }) => { (result, { adjusted_close, date }) => {
@ -223,12 +223,14 @@ export class EodHistoricalDataService
s: eodHistoricalDataSymbols.join(',') s: eodHistoricalDataSymbols.join(',')
}); });
const realTimeResponse = await fetch( const realTimeResponse = await this.fetchService
.fetch(
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`, `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
const quotes: { const quotes: {
close: number; close: number;
@ -430,12 +432,11 @@ export class EodHistoricalDataService
api_token: this.apiKey api_token: this.apiKey
}); });
const response = await fetch( const response = await this.fetchService
`${this.URL}/search/${query}?${queryParams.toString()}`, .fetch(`${this.URL}/search/${query}?${queryParams.toString()}`, {
{
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} })
).then((res) => res.json()); .then((res) => res.json());
searchResult = response.map( searchResult = response.map(
({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => { ({ Code, Currency, Exchange, ISIN: isin, Name: name, Type }) => {

77
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -9,6 +9,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
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 { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -59,6 +60,7 @@ export class FinancialModelingPrepService
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService, private readonly cryptocurrencyService: CryptocurrencyService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService private readonly prismaService: PrismaService
) {} ) {}
@ -96,12 +98,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey apikey: this.apiKey
}); });
const [quote] = await fetch( const [quote] = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/quote?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/quote?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
response.assetClass = AssetClass.LIQUIDITY; response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CRYPTOCURRENCY; response.assetSubClass = AssetSubClass.CRYPTOCURRENCY;
@ -115,12 +119,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey apikey: this.apiKey
}); });
const [assetProfile] = await fetch( const [assetProfile] = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/profile?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/profile?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
if (!assetProfile) { if (!assetProfile) {
throw new AssetProfileDelistedError( throw new AssetProfileDelistedError(
@ -143,12 +149,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey apikey: this.apiKey
}); });
const etfCountryWeightings = await fetch( const etfCountryWeightings = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/etf/country-weightings?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/etf/country-weightings?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
response.countries = etfCountryWeightings response.countries = etfCountryWeightings
.filter(({ country: countryName }) => { .filter(({ country: countryName }) => {
@ -174,12 +182,14 @@ export class FinancialModelingPrepService
}; };
}); });
const etfHoldings = await fetch( const etfHoldings = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/etf/holdings?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/etf/holdings?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
const sortedTopHoldings = etfHoldings const sortedTopHoldings = etfHoldings
.sort((a, b) => { .sort((a, b) => {
@ -193,23 +203,27 @@ export class FinancialModelingPrepService
} }
); );
const [etfInformation] = await fetch( const [etfInformation] = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/etf/info?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/etf/info?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
if (etfInformation?.website) { if (etfInformation?.website) {
response.url = etfInformation.website; response.url = etfInformation.website;
} }
const etfSectorWeightings = await fetch( const etfSectorWeightings = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/etf/sector-weightings?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/etf/sector-weightings?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
response.sectors = etfSectorWeightings.map( response.sectors = etfSectorWeightings.map(
({ sector, weightPercentage }) => { ({ sector, weightPercentage }) => {
@ -286,12 +300,14 @@ export class FinancialModelingPrepService
[date: string]: DataProviderHistoricalResponse; [date: string]: DataProviderHistoricalResponse;
} = {}; } = {};
const dividends = await fetch( const dividends = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/dividends?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/dividends?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
dividends dividends
.filter(({ date }) => { .filter(({ date }) => {
@ -354,12 +370,14 @@ export class FinancialModelingPrepService
to: format(currentTo, DATE_FORMAT) to: format(currentTo, DATE_FORMAT)
}); });
const historical = await fetch( const historical = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/historical-price-eod/full?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/historical-price-eod/full?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
for (const { close, date } of historical) { for (const { close, date } of historical) {
if ( if (
@ -422,13 +440,16 @@ export class FinancialModelingPrepService
symbolTarget: { in: symbols } symbolTarget: { in: symbols }
} }
}), }),
fetch( this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/batch-quote-short?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/batch-quote-short?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then( )
(res) => res.json() as unknown as { price: number; symbol: string }[] .then(
(res) =>
res.json() as unknown as { price: number; symbol: string }[]
) )
]); ]);
@ -525,12 +546,14 @@ export class FinancialModelingPrepService
isin: query.toUpperCase() isin: query.toUpperCase()
}); });
const result = await fetch( const result = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-isin?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/search-isin?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()); )
.then((res) => res.json());
await Promise.all( await Promise.all(
result.map(({ symbol }) => { result.map(({ symbol }) => {
@ -558,18 +581,22 @@ export class FinancialModelingPrepService
}); });
const [nameResults, symbolResults] = await Promise.all([ const [nameResults, symbolResults] = await Promise.all([
fetch( this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-name?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/search-name?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()), )
fetch( .then((res) => res.json()),
this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-symbol?${queryParams.toString()}`, `${this.getUrl({ version: 'stable' })}/search-symbol?${queryParams.toString()}`,
{ {
signal: AbortSignal.timeout(requestTimeout) signal: AbortSignal.timeout(requestTimeout)
} }
).then((res) => res.json()) )
.then((res) => res.json())
]); ]);
const result = uniqBy( const result = uniqBy(

12
apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts

@ -8,6 +8,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
HEADER_KEY_TOKEN, HEADER_KEY_TOKEN,
@ -38,6 +39,7 @@ export class GhostfolioService implements DataProviderInterface {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly propertyService: PropertyService private readonly propertyService: PropertyService
) {} ) {}
@ -52,7 +54,7 @@ export class GhostfolioService implements DataProviderInterface {
let assetProfile: DataProviderGhostfolioAssetProfileResponse; let assetProfile: DataProviderGhostfolioAssetProfileResponse;
try { try {
const response = await fetch( const response = await this.fetchService.fetch(
`${this.URL}/v1/data-providers/ghostfolio/asset-profile/${symbol}`, `${this.URL}/v1/data-providers/ghostfolio/asset-profile/${symbol}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
@ -122,7 +124,7 @@ export class GhostfolioService implements DataProviderInterface {
to: format(to, DATE_FORMAT) to: format(to, DATE_FORMAT)
}); });
const response = await fetch( const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?${queryParams.toString()}`, `${this.URL}/v2/data-providers/ghostfolio/dividends/${symbol}?${queryParams.toString()}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
@ -174,7 +176,7 @@ export class GhostfolioService implements DataProviderInterface {
to: format(to, DATE_FORMAT) to: format(to, DATE_FORMAT)
}); });
const response = await fetch( const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?${queryParams.toString()}`, `${this.URL}/v2/data-providers/ghostfolio/historical/${symbol}?${queryParams.toString()}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
@ -245,7 +247,7 @@ export class GhostfolioService implements DataProviderInterface {
symbols: symbols.join(',') symbols: symbols.join(',')
}); });
const response = await fetch( const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/quotes?${queryParams.toString()}`, `${this.URL}/v2/data-providers/ghostfolio/quotes?${queryParams.toString()}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),
@ -302,7 +304,7 @@ export class GhostfolioService implements DataProviderInterface {
query query
}); });
const response = await fetch( const response = await this.fetchService.fetch(
`${this.URL}/v2/data-providers/ghostfolio/lookup?${queryParams.toString()}`, `${this.URL}/v2/data-providers/ghostfolio/lookup?${queryParams.toString()}`,
{ {
headers: await this.getRequestHeaders(), headers: await this.getRequestHeaders(),

4
apps/api/src/services/data-provider/manual/manual.service.ts

@ -8,6 +8,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
@ -32,6 +33,7 @@ import { addDays, format, isBefore } from 'date-fns';
export class ManualService implements DataProviderInterface { export class ManualService implements DataProviderInterface {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -292,7 +294,7 @@ export class ManualService implements DataProviderInterface {
}): Promise<number> { }): Promise<number> {
let locale = scraperConfiguration.locale; let locale = scraperConfiguration.locale;
const response = await fetch(scraperConfiguration.url, { const response = await this.fetchService.fetch(scraperConfiguration.url, {
headers: scraperConfiguration.headers as HeadersInit, headers: scraperConfiguration.headers as HeadersInit,
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')

13
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -7,6 +7,7 @@ import {
GetQuotesParams, GetQuotesParams,
GetSearchParams GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { import {
ghostfolioFearAndGreedIndexSymbol, ghostfolioFearAndGreedIndexSymbol,
ghostfolioFearAndGreedIndexSymbolStocks ghostfolioFearAndGreedIndexSymbolStocks
@ -26,7 +27,8 @@ import { format } from 'date-fns';
@Injectable() @Injectable()
export class RapidApiService implements DataProviderInterface { export class RapidApiService implements DataProviderInterface {
public constructor( public constructor(
private readonly configurationService: ConfigurationService private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {} ) {}
public canHandle() { public canHandle() {
@ -142,9 +144,8 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string }; oneYearAgo: { value: number; valueText: string };
}> { }> {
try { try {
const { fgi } = await fetch( const { fgi } = await this.fetchService
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, .fetch(`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, {
{
headers: { headers: {
useQueryString: 'true', useQueryString: 'true',
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com', 'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
@ -153,8 +154,8 @@ export class RapidApiService implements DataProviderInterface {
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} })
).then((res) => res.json()); .then((res) => res.json());
return fgi; return fgi;
} catch (error) { } catch (error) {

9
apps/api/src/services/fetch/fetch.module.ts

@ -0,0 +1,9 @@
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { Module } from '@nestjs/common';
@Module({
exports: [FetchService],
providers: [FetchService]
})
export class FetchModule {}

63
apps/api/src/services/fetch/fetch.service.ts

@ -0,0 +1,63 @@
import { redactPaths } from '@ghostfolio/api/helper/object.helper';
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class FetchService {
private static readonly REDACTED_QUERY_PARAM_NAMES = ['apikey', 'api_token'];
public async fetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const method = (
init?.method ??
(input instanceof Request ? input.method : undefined) ??
'GET'
).toUpperCase();
const url = input instanceof Request ? input.url : input.toString();
const urlRedacted = this.redactUrl(url);
Logger.debug(`${method} ${urlRedacted}`, 'FetchService');
try {
return await globalThis.fetch(input, init);
} catch (error) {
if (error instanceof Error) {
Logger.error(
`${method} ${urlRedacted} failed: [${error.name}] ${error.message}`,
'FetchService'
);
} else {
Logger.error(
`${method} ${urlRedacted} failed: ${String(error)}`,
'FetchService'
);
}
throw error;
}
}
private redactUrl(rawUrl: string): string {
try {
const url = new URL(rawUrl);
const redacted = redactPaths({
object: Object.fromEntries(url.searchParams),
paths: FetchService.REDACTED_QUERY_PARAM_NAMES
});
for (const [key, value] of Object.entries(redacted)) {
if (value === null) {
url.searchParams.set(key, '*******');
}
}
return url.toString();
} catch {
return rawUrl;
}
}
}

2
apps/api/src/services/queues/statistics-gathering/statistics-gathering.module.ts

@ -1,4 +1,5 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { STATISTICS_GATHERING_QUEUE } from '@ghostfolio/common/config'; import { STATISTICS_GATHERING_QUEUE } from '@ghostfolio/common/config';
@ -29,6 +30,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service';
name: STATISTICS_GATHERING_QUEUE name: STATISTICS_GATHERING_QUEUE
}), }),
ConfigurationModule, ConfigurationModule,
FetchModule,
PropertyModule PropertyModule
], ],
providers: [StatisticsGatheringProcessor, StatisticsGatheringService] providers: [StatisticsGatheringProcessor, StatisticsGatheringService]

32
apps/api/src/services/queues/statistics-gathering/statistics-gathering.processor.ts

@ -1,4 +1,5 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME, GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME,
@ -28,6 +29,7 @@ import { format, subDays } from 'date-fns';
export class StatisticsGatheringProcessor { export class StatisticsGatheringProcessor {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly propertyService: PropertyService private readonly propertyService: PropertyService
) {} ) {}
@ -126,15 +128,14 @@ export class StatisticsGatheringProcessor {
private async countDockerHubPulls(): Promise<number> { private async countDockerHubPulls(): Promise<number> {
try { try {
const { pull_count } = (await fetch( const { pull_count } = (await this.fetchService
'https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', .fetch('https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', {
{
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} })
).then((res) => res.json())) as { pull_count: number }; .then((res) => res.json())) as { pull_count: number };
return pull_count; return pull_count;
} catch (error) { } catch (error) {
@ -146,11 +147,13 @@ export class StatisticsGatheringProcessor {
private async countGitHubContributors(): Promise<number> { private async countGitHubContributors(): Promise<number> {
try { try {
const body = await fetch('https://github.com/ghostfolio/ghostfolio', { const body = await this.fetchService
.fetch('https://github.com/ghostfolio/ghostfolio', {
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
}).then((res) => res.text()); })
.then((res) => res.text());
const $ = cheerio.load(body); const $ = cheerio.load(body);
@ -174,15 +177,14 @@ export class StatisticsGatheringProcessor {
private async countGitHubStargazers(): Promise<number> { private async countGitHubStargazers(): Promise<number> {
try { try {
const { stargazers_count } = (await fetch( const { stargazers_count } = (await this.fetchService
'https://api.github.com/repos/ghostfolio/ghostfolio', .fetch('https://api.github.com/repos/ghostfolio/ghostfolio', {
{
headers: { 'User-Agent': 'request' }, headers: { 'User-Agent': 'request' },
signal: AbortSignal.timeout( signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} })
).then((res) => res.json())) as { stargazers_count: number }; .then((res) => res.json())) as { stargazers_count: number };
return stargazers_count; return stargazers_count;
} catch (error) { } catch (error) {
@ -194,7 +196,8 @@ export class StatisticsGatheringProcessor {
private async getUptime(monitorId: string): Promise<number> { private async getUptime(monitorId: string): Promise<number> {
try { try {
const { data } = await fetch( const { data } = await this.fetchService
.fetch(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format( `https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90), subDays(new Date(), 90),
DATE_FORMAT DATE_FORMAT
@ -209,7 +212,8 @@ export class StatisticsGatheringProcessor {
this.configurationService.get('REQUEST_TIMEOUT') this.configurationService.get('REQUEST_TIMEOUT')
) )
} }
).then((res) => res.json()); )
.then((res) => res.json());
return data.attributes.availability / 100; return data.attributes.availability / 100;
} catch (error) { } catch (error) {

Loading…
Cancel
Save