diff --git a/CHANGELOG.md b/CHANGELOG.md index 541a0d4ec..717cf2813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Migrated the login with access token dialog from `ngModel` to form control + +## 2.193.0 - 2025-08-22 + ### Added +- Added a filter by data source for the asset profiles in the admin control panel - Extended the data providers management of the admin control panel by every data provider in use ### Changed -- Migrated the login with access token dialog from `ngModel` to form control +- Improved the error handling in data providers +- Upgraded `yahoo-finance2` from version `3.4.1` to `3.6.4` ## 2.192.0 - 2025-08-21 diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e473813e9..27cc088d1 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -197,6 +197,7 @@ export class AdminController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) public async getMarketData( @Query('assetSubClasses') filterByAssetSubClasses?: string, + @Query('dataSource') filterByDataSource?: string, @Query('presetId') presetId?: MarketDataPreset, @Query('query') filterBySearchQuery?: string, @Query('skip') skip?: number, @@ -206,6 +207,7 @@ export class AdminController { ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAssetSubClasses, + filterByDataSource, filterBySearchQuery }); diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index bce603289..d07e74013 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -218,12 +218,12 @@ export class AdminService { return type === 'SEARCH_QUERY'; })?.id; - const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( - filters, - ({ type }) => { - return type; - } - ); + const { + ASSET_SUB_CLASS: filtersByAssetSubClass, + DATA_SOURCE: filtersByDataSource + } = groupBy(filters, ({ type }) => { + return type; + }); const marketDataItems = await this.prismaService.marketData.groupBy({ _count: true, @@ -234,6 +234,10 @@ export class AdminService { where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; } + if (filtersByDataSource) { + where.dataSource = DataSource[filtersByDataSource[0].id]; + } + if (searchQuery) { where.OR = [ { id: { mode: 'insensitive', startsWith: searchQuery } }, diff --git a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts index 7776ff46c..8a3adb507 100644 --- a/apps/api/src/services/data-provider/coingecko/coingecko.service.ts +++ b/apps/api/src/services/data-provider/coingecko/coingecko.service.ts @@ -78,7 +78,7 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; @@ -196,8 +196,10 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; } @@ -237,7 +239,7 @@ export class CoinGeckoService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index ddb94bb1a..d06071ac3 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -282,8 +282,10 @@ export class EodHistoricalDataService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; } @@ -426,7 +428,7 @@ export class EodHistoricalDataService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${aQuery} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts index 2dcb689a7..ed2aa5f25 100644 --- a/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts +++ b/apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts @@ -202,7 +202,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; @@ -392,8 +392,10 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; } @@ -469,7 +471,7 @@ export class FinancialModelingPrepService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts index 3fd9e1bea..48ba42bd4 100644 --- a/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts @@ -68,14 +68,16 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; - } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { @@ -229,14 +231,18 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { - message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( + if (['AbortError', 'TimeoutError'].includes(error?.name)) { + message = `RequestError: The operation to get the quotes for ${symbols.join( + ', ' + )} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; - } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { @@ -272,14 +278,16 @@ export class GhostfolioService implements DataProviderInterface { } catch (error) { let message = error; - if (error.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${( requestTimeout / 1000 ).toFixed(3)} seconds`; - } else if (error.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS) { + } else if ( + error?.response?.statusCode === StatusCodes.TOO_MANY_REQUESTS + ) { message = 'RequestError: The daily request limit has been exceeded'; } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { - if (!error.request?.options?.headers?.authorization?.includes('-')) { + if (!error?.request?.options?.headers?.authorization?.includes('-')) { message = 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; } else { diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index 5675f1eb0..62b3ed71c 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -165,7 +165,7 @@ export class RapidApiService implements DataProviderInterface { } catch (error) { let message = error; - if (error?.name === 'AbortError') { + if (['AbortError', 'TimeoutError'].includes(error?.name)) { message = `RequestError: The operation was aborted because the request to the data provider took more than ${( this.configurationService.get('REQUEST_TIMEOUT') / 1000 ).toFixed(3)} seconds`; diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 6a809a10f..4e410c3a0 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts @@ -103,39 +103,46 @@ export class GfAdminMarketDataComponent @ViewChild(MatSort) sort: MatSort; public activeFilters: Filter[] = []; - public allFilters: Filter[] = Object.keys(AssetSubClass) - .filter((assetSubClass) => { - return assetSubClass !== 'CASH'; - }) - .map((assetSubClass) => { + public allFilters: Filter[] = [ + ...Object.keys(AssetSubClass) + .filter((assetSubClass) => { + return assetSubClass !== 'CASH'; + }) + .map((assetSubClass) => { + return { + id: assetSubClass.toString(), + label: translate(assetSubClass), + type: 'ASSET_SUB_CLASS' as Filter['type'] + }; + }), + ...Object.keys(DataSource).map((dataSource) => { return { - id: assetSubClass.toString(), - label: translate(assetSubClass), - type: 'ASSET_SUB_CLASS' as Filter['type'] + id: dataSource.toString(), + label: dataSource, + type: 'DATA_SOURCE' as Filter['type'] }; - }) - .concat([ - { - id: 'BENCHMARKS', - label: $localize`Benchmarks`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'CURRENCIES', - label: $localize`Currencies`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'ETF_WITHOUT_COUNTRIES', - label: $localize`ETFs without Countries`, - type: 'PRESET_ID' as Filter['type'] - }, - { - id: 'ETF_WITHOUT_SECTORS', - label: $localize`ETFs without Sectors`, - type: 'PRESET_ID' as Filter['type'] - } - ]); + }), + { + id: 'BENCHMARKS', + label: $localize`Benchmarks`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'CURRENCIES', + label: $localize`Currencies`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'ETF_WITHOUT_COUNTRIES', + label: $localize`ETFs without Countries`, + type: 'PRESET_ID' as Filter['type'] + }, + { + id: 'ETF_WITHOUT_SECTORS', + label: $localize`ETFs without Sectors`, + type: 'PRESET_ID' as Filter['type'] + } + ]; public benchmarks: Partial[]; public currentDataSource: DataSource; public currentSymbol: string; diff --git a/libs/ui/src/lib/i18n.ts b/libs/ui/src/lib/i18n.ts index 8007dc53e..e17cc6771 100644 --- a/libs/ui/src/lib/i18n.ts +++ b/libs/ui/src/lib/i18n.ts @@ -12,6 +12,7 @@ const locales = { DATA_IMPORT_AND_EXPORT_TOOLTIP_BASIC: $localize`Switch to Ghostfolio Premium or Ghostfolio Open Source easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_OSS: $localize`Switch to Ghostfolio Premium easily`, DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic easily`, + DATA_SOURCE: $localize`Data Source`, EMERGENCY_FUND: $localize`Emergency Fund`, EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`, Global: $localize`Global`, diff --git a/package-lock.json b/package-lock.json index 690ab659c..a86aa3c10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -93,7 +93,7 @@ "svgmap": "2.12.2", "twitter-api-v2": "1.23.0", "uuid": "11.1.0", - "yahoo-finance2": "3.4.1", + "yahoo-finance2": "3.6.4", "zone.js": "0.15.1" }, "devDependencies": { @@ -40601,9 +40601,9 @@ } }, "node_modules/yahoo-finance2": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.4.1.tgz", - "integrity": "sha512-L8Ubmdsn6f+uJEuEDUUHR5n95TFcGkMiMkV0phmvPONFekAn1vWzsEzGfIDG2ODR7aYBB+aURdQg7a3HX2iUHA==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.6.4.tgz", + "integrity": "sha512-IoMU8Hb4BEaNPVnamZjRBuorTGDbaaiV/tM/m3KI8dzwrR6BGmeuT40OX+5IqRiSkMlD8g0kAwGi9E4bY3rLvg==", "license": "MIT", "dependencies": { "@deno/shim-deno": "~0.18.0", diff --git a/package.json b/package.json index 34f975ff6..48a79812d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.192.0", + "version": "2.193.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -139,7 +139,7 @@ "svgmap": "2.12.2", "twitter-api-v2": "1.23.0", "uuid": "11.1.0", - "yahoo-finance2": "3.4.1", + "yahoo-finance2": "3.6.4", "zone.js": "0.15.1" }, "devDependencies": {