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
### Added
- Added the `FetchService` to centralize outbound HTTP requests
### Changed
- 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 { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
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 { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -23,6 +25,7 @@ import { OidcStrategy } from './oidc.strategy';
controllers: [AuthController],
imports: [
ConfigurationModule,
FetchModule,
JwtModule.register({
secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' }
@ -40,11 +43,12 @@ import { OidcStrategy } from './oidc.strategy';
GoogleStrategy,
JwtStrategy,
{
inject: [AuthService, ConfigurationService],
inject: [AuthService, ConfigurationService, FetchService],
provide: OidcStrategy,
useFactory: async (
authService: AuthService,
configurationService: ConfigurationService
configurationService: ConfigurationService,
fetchService: FetchService
) => {
const isOidcEnabled = configurationService.get(
'ENABLE_FEATURE_AUTH_OIDC'
@ -81,7 +85,7 @@ import { OidcStrategy } from './oidc.strategy';
} else {
// Fetch OIDC configuration from discovery endpoint
try {
const response = await fetch(
const response = await fetchService.fetch(
`${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 { 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 { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -27,6 +28,7 @@ import { GhostfolioService } from './ghostfolio.service';
imports: [
CryptocurrencyModule,
DataProviderModule,
FetchModule,
MarketDataModule,
PrismaModule,
PropertyModule,

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

@ -8,6 +8,7 @@ import {
GetQuotesParams,
GetSearchParams
} 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 { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
@ -36,6 +37,7 @@ export class GhostfolioService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService
) {}
@ -355,6 +357,7 @@ export class GhostfolioService {
private getDataProviderInfo(): DataProviderInfo {
const ghostfolioDataProviderService = new GhostfolioDataProviderService(
this.configurationService,
this.fetchService,
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 { 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 { Module } from '@nestjs/common';
@ -11,6 +12,7 @@ import { LogoService } from './logo.service';
controllers: [LogoController],
imports: [
ConfigurationModule,
FetchModule,
SymbolProfileModule,
TransformDataSourceInRequestModule
],

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

@ -1,4 +1,5 @@
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 { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
@ -10,6 +11,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
export class LogoService {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -43,7 +45,8 @@ export class LogoService {
}
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`,
{
headers: { 'User-Agent': 'request' },
@ -51,7 +54,8 @@ export class LogoService {
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.blob());
)
.then((res) => res.blob());
return {
buffer: await blob.arrayBuffer().then((arrayBuffer) => {

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

@ -7,6 +7,7 @@ import {
GetQuotesParams,
GetSearchParams
} 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 { DATE_FORMAT } from '@ghostfolio/common/helper';
import {
@ -32,7 +33,8 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
private headers: HeadersInit = {};
public constructor(
private readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {}
public onModuleInit() {
@ -67,12 +69,14 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
};
try {
const { name } = await fetch(`${this.apiUrl}/coins/${symbol}`, {
const { name } = await this.fetchService
.fetch(`${this.apiUrl}/coins/${symbol}`, {
headers: this.headers,
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}).then((res) => res.json());
})
.then((res) => res.json());
response.name = name;
} catch (error) {
@ -118,13 +122,15 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
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()}`,
{
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
if (error?.status) {
throw new Error(error.status.error_message);
@ -181,13 +187,12 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
vs_currencies: DEFAULT_CURRENCY.toLowerCase()
});
const quotes = await fetch(
`${this.apiUrl}/simple/price?${queryParams.toString()}`,
{
const quotes = await this.fetchService
.fetch(`${this.apiUrl}/simple/price?${queryParams.toString()}`, {
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
})
.then((res) => res.json());
for (const symbol in quotes) {
response[symbol] = {
@ -230,13 +235,12 @@ export class CoinGeckoService implements DataProviderInterface, OnModuleInit {
query
});
const { coins } = await fetch(
`${this.apiUrl}/search?${queryParams.toString()}`,
{
const { coins } = await this.fetchService
.fetch(`${this.apiUrl}/search?${queryParams.toString()}`, {
headers: this.headers,
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
})
.then((res) => res.json());
items = coins.map(({ id: symbol, name }) => {
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 { 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 { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { Module } from '@nestjs/common';
@ -16,7 +17,7 @@ import { DataEnhancerService } from './data-enhancer.service';
YahooFinanceDataEnhancerService,
'DataEnhancers'
],
imports: [ConfigurationModule, CryptocurrencyModule],
imports: [ConfigurationModule, CryptocurrencyModule, FetchModule],
providers: [
DataEnhancerService,
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 { 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 { Injectable } from '@nestjs/common';
@ -10,7 +11,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://api.openfigi.com';
public constructor(
private readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {}
public async enhance({
@ -42,9 +44,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
this.configurationService.get('API_KEY_OPEN_FIGI');
}
const mappings = (await fetch(
`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`,
{
const mappings = (await this.fetchService
.fetch(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {
body: JSON.stringify([
{ exchCode: exchange, idType: 'TICKER', idValue: ticker }
]),
@ -54,8 +55,8 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
},
method: 'POST',
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) {
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 { 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 { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
@ -23,7 +24,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
};
public constructor(
private readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {}
public async enhance({
@ -60,7 +62,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response;
}
const profile = await fetch(
const profile = await this.fetchService
.fetch(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${trackinsightSymbol}.json`,
{
signal: AbortSignal.timeout(requestTimeout)
@ -83,7 +86,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
response.isin = isin;
}
const holdings = await fetch(
const holdings = await this.fetchService
.fetch(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${trackinsightSymbol}.json`,
{
signal: AbortSignal.timeout(requestTimeout)
@ -182,7 +186,8 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
requestTimeout: number;
symbol: string;
}) {
return fetch(
return this.fetchService
.fetch(
`https://www.trackinsight.com/search-api/search_v2/${symbol}/_/ticker/default/0/3`,
{
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 { 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 { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
@ -26,6 +27,7 @@ import { DataProviderService } from './data-provider.service';
ConfigurationModule,
CryptocurrencyModule,
DataEnhancerModule,
FetchModule,
MarketDataModule,
PrismaModule,
PropertyModule,

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

@ -7,6 +7,7 @@ import {
GetQuotesParams,
GetSearchParams
} 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 {
DEFAULT_CURRENCY,
@ -41,6 +42,7 @@ export class EodHistoricalDataService
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -111,12 +113,11 @@ export class EodHistoricalDataService
[date: string]: DataProviderHistoricalResponse;
} = {};
const historicalResult = await fetch(
`${this.URL}/div/${symbol}?${queryParams.toString()}`,
{
const historicalResult = await this.fetchService
.fetch(`${this.URL}/div/${symbol}?${queryParams.toString()}`, {
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
})
.then((res) => res.json());
for (const { date, value } of historicalResult) {
response[date] = {
@ -158,12 +159,11 @@ export class EodHistoricalDataService
to: format(to, DATE_FORMAT)
});
const response = await fetch(
`${this.URL}/eod/${symbol}?${queryParams.toString()}`,
{
const response = await this.fetchService
.fetch(`${this.URL}/eod/${symbol}?${queryParams.toString()}`, {
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
})
.then((res) => res.json());
return response.reduce(
(result, { adjusted_close, date }) => {
@ -223,12 +223,14 @@ export class EodHistoricalDataService
s: eodHistoricalDataSymbols.join(',')
});
const realTimeResponse = await fetch(
const realTimeResponse = await this.fetchService
.fetch(
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
const quotes: {
close: number;
@ -430,12 +432,11 @@ export class EodHistoricalDataService
api_token: this.apiKey
});
const response = await fetch(
`${this.URL}/search/${query}?${queryParams.toString()}`,
{
const response = await this.fetchService
.fetch(`${this.URL}/search/${query}?${queryParams.toString()}`, {
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
})
.then((res) => res.json());
searchResult = response.map(
({ 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,
GetSearchParams
} 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 {
DEFAULT_CURRENCY,
@ -59,6 +60,7 @@ export class FinancialModelingPrepService
public constructor(
private readonly configurationService: ConfigurationService,
private readonly cryptocurrencyService: CryptocurrencyService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService
) {}
@ -96,12 +98,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey
});
const [quote] = await fetch(
const [quote] = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/quote?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
response.assetClass = AssetClass.LIQUIDITY;
response.assetSubClass = AssetSubClass.CRYPTOCURRENCY;
@ -115,12 +119,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey
});
const [assetProfile] = await fetch(
const [assetProfile] = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/profile?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
if (!assetProfile) {
throw new AssetProfileDelistedError(
@ -143,12 +149,14 @@ export class FinancialModelingPrepService
apikey: this.apiKey
});
const etfCountryWeightings = await fetch(
const etfCountryWeightings = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/etf/country-weightings?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
response.countries = etfCountryWeightings
.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()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
const sortedTopHoldings = etfHoldings
.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()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
if (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()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
response.sectors = etfSectorWeightings.map(
({ sector, weightPercentage }) => {
@ -286,12 +300,14 @@ export class FinancialModelingPrepService
[date: string]: DataProviderHistoricalResponse;
} = {};
const dividends = await fetch(
const dividends = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/dividends?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
dividends
.filter(({ date }) => {
@ -354,12 +370,14 @@ export class FinancialModelingPrepService
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()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
for (const { close, date } of historical) {
if (
@ -422,13 +440,16 @@ export class FinancialModelingPrepService
symbolTarget: { in: symbols }
}
}),
fetch(
this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/batch-quote-short?${queryParams.toString()}`,
{
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()
});
const result = await fetch(
const result = await this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-isin?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json());
)
.then((res) => res.json());
await Promise.all(
result.map(({ symbol }) => {
@ -558,18 +581,22 @@ export class FinancialModelingPrepService
});
const [nameResults, symbolResults] = await Promise.all([
fetch(
this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-name?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json()),
fetch(
)
.then((res) => res.json()),
this.fetchService
.fetch(
`${this.getUrl({ version: 'stable' })}/search-symbol?${queryParams.toString()}`,
{
signal: AbortSignal.timeout(requestTimeout)
}
).then((res) => res.json())
)
.then((res) => res.json())
]);
const result = uniqBy(

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

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

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

@ -8,6 +8,7 @@ import {
GetQuotesParams,
GetSearchParams
} 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 { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import {
@ -32,6 +33,7 @@ import { addDays, format, isBefore } from 'date-fns';
export class ManualService implements DataProviderInterface {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -292,7 +294,7 @@ export class ManualService implements DataProviderInterface {
}): Promise<number> {
let locale = scraperConfiguration.locale;
const response = await fetch(scraperConfiguration.url, {
const response = await this.fetchService.fetch(scraperConfiguration.url, {
headers: scraperConfiguration.headers as HeadersInit,
signal: AbortSignal.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,
GetSearchParams
} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import {
ghostfolioFearAndGreedIndexSymbol,
ghostfolioFearAndGreedIndexSymbolStocks
@ -26,7 +27,8 @@ import { format } from 'date-fns';
@Injectable()
export class RapidApiService implements DataProviderInterface {
public constructor(
private readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService
) {}
public canHandle() {
@ -142,9 +144,8 @@ export class RapidApiService implements DataProviderInterface {
oneYearAgo: { value: number; valueText: string };
}> {
try {
const { fgi } = await fetch(
`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`,
{
const { fgi } = await this.fetchService
.fetch(`https://fear-and-greed-index.p.rapidapi.com/v1/fgi`, {
headers: {
useQueryString: 'true',
'x-rapidapi-host': 'fear-and-greed-index.p.rapidapi.com',
@ -153,8 +154,8 @@ export class RapidApiService implements DataProviderInterface {
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
})
.then((res) => res.json());
return fgi;
} 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 { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { STATISTICS_GATHERING_QUEUE } from '@ghostfolio/common/config';
@ -29,6 +30,7 @@ import { StatisticsGatheringService } from './statistics-gathering.service';
name: STATISTICS_GATHERING_QUEUE
}),
ConfigurationModule,
FetchModule,
PropertyModule
],
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 { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import {
GATHER_STATISTICS_DOCKER_HUB_PULLS_PROCESS_JOB_NAME,
@ -28,6 +29,7 @@ import { format, subDays } from 'date-fns';
export class StatisticsGatheringProcessor {
public constructor(
private readonly configurationService: ConfigurationService,
private readonly fetchService: FetchService,
private readonly propertyService: PropertyService
) {}
@ -126,15 +128,14 @@ export class StatisticsGatheringProcessor {
private async countDockerHubPulls(): Promise<number> {
try {
const { pull_count } = (await fetch(
'https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio',
{
const { pull_count } = (await this.fetchService
.fetch('https://hub.docker.com/v2/repositories/ghostfolio/ghostfolio', {
headers: { 'User-Agent': 'request' },
signal: AbortSignal.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;
} catch (error) {
@ -146,11 +147,13 @@ export class StatisticsGatheringProcessor {
private async countGitHubContributors(): Promise<number> {
try {
const body = await fetch('https://github.com/ghostfolio/ghostfolio', {
const body = await this.fetchService
.fetch('https://github.com/ghostfolio/ghostfolio', {
signal: AbortSignal.timeout(
this.configurationService.get('REQUEST_TIMEOUT')
)
}).then((res) => res.text());
})
.then((res) => res.text());
const $ = cheerio.load(body);
@ -174,15 +177,14 @@ export class StatisticsGatheringProcessor {
private async countGitHubStargazers(): Promise<number> {
try {
const { stargazers_count } = (await fetch(
'https://api.github.com/repos/ghostfolio/ghostfolio',
{
const { stargazers_count } = (await this.fetchService
.fetch('https://api.github.com/repos/ghostfolio/ghostfolio', {
headers: { 'User-Agent': 'request' },
signal: AbortSignal.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;
} catch (error) {
@ -194,7 +196,8 @@ export class StatisticsGatheringProcessor {
private async getUptime(monitorId: string): Promise<number> {
try {
const { data } = await fetch(
const { data } = await this.fetchService
.fetch(
`https://uptime.betterstack.com/api/v2/monitors/${monitorId}/sla?from=${format(
subDays(new Date(), 90),
DATE_FORMAT
@ -209,7 +212,8 @@ export class StatisticsGatheringProcessor {
this.configurationService.get('REQUEST_TIMEOUT')
)
}
).then((res) => res.json());
)
.then((res) => res.json());
return data.attributes.availability / 100;
} catch (error) {

Loading…
Cancel
Save