Browse Source

Merge branch 'main' into task/migrate-login-dialog-to-form-control

pull/5390/head
Thomas Kaul 15 hours ago
committed by GitHub
parent
commit
271ab0c8ff
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      CHANGELOG.md
  2. 2
      apps/api/src/app/admin/admin.controller.ts
  3. 16
      apps/api/src/app/admin/admin.service.ts
  4. 10
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  5. 8
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  6. 10
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  7. 34
      apps/api/src/services/data-provider/ghostfolio/ghostfolio.service.ts
  8. 2
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  9. 69
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  10. 1
      libs/ui/src/lib/i18n.ts
  11. 12
      package-lock.json
  12. 4
      package.json

10
CHANGELOG.md

@ -7,13 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Migrated the login with access token dialog from `ngModel` to form control
## 2.193.0 - 2025-08-22
### Added ### 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 - Extended the data providers management of the admin control panel by every data provider in use
### Changed ### 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 ## 2.192.0 - 2025-08-21

2
apps/api/src/app/admin/admin.controller.ts

@ -197,6 +197,7 @@ export class AdminController {
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getMarketData( public async getMarketData(
@Query('assetSubClasses') filterByAssetSubClasses?: string, @Query('assetSubClasses') filterByAssetSubClasses?: string,
@Query('dataSource') filterByDataSource?: string,
@Query('presetId') presetId?: MarketDataPreset, @Query('presetId') presetId?: MarketDataPreset,
@Query('query') filterBySearchQuery?: string, @Query('query') filterBySearchQuery?: string,
@Query('skip') skip?: number, @Query('skip') skip?: number,
@ -206,6 +207,7 @@ export class AdminController {
): Promise<AdminMarketData> { ): Promise<AdminMarketData> {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAssetSubClasses, filterByAssetSubClasses,
filterByDataSource,
filterBySearchQuery filterBySearchQuery
}); });

16
apps/api/src/app/admin/admin.service.ts

@ -218,12 +218,12 @@ export class AdminService {
return type === 'SEARCH_QUERY'; return type === 'SEARCH_QUERY';
})?.id; })?.id;
const { ASSET_SUB_CLASS: filtersByAssetSubClass } = groupBy( const {
filters, ASSET_SUB_CLASS: filtersByAssetSubClass,
({ type }) => { DATA_SOURCE: filtersByDataSource
return type; } = groupBy(filters, ({ type }) => {
} return type;
); });
const marketDataItems = await this.prismaService.marketData.groupBy({ const marketDataItems = await this.prismaService.marketData.groupBy({
_count: true, _count: true,
@ -234,6 +234,10 @@ export class AdminService {
where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id]; where.assetSubClass = AssetSubClass[filtersByAssetSubClass[0].id];
} }
if (filtersByDataSource) {
where.dataSource = DataSource[filtersByDataSource[0].id];
}
if (searchQuery) { if (searchQuery) {
where.OR = [ where.OR = [
{ id: { mode: 'insensitive', startsWith: searchQuery } }, { id: { mode: 'insensitive', startsWith: searchQuery } },

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

@ -78,7 +78,7 @@ export class CoinGeckoService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -196,8 +196,10 @@ export class CoinGeckoService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.name === 'AbortError') { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
@ -237,7 +239,7 @@ export class CoinGeckoService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

8
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) { } catch (error) {
let message = error; let message = error;
if (error?.name === 'AbortError') { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
@ -426,7 +428,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

10
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) { } catch (error) {
let message = 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 ${( 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 requestTimeout / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
@ -392,8 +392,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error?.name === 'AbortError') { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( 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 requestTimeout / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;
} }
@ -469,7 +471,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( 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 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

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

@ -68,14 +68,16 @@ export class GhostfolioService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error.name === 'AbortError') { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( 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 requestTimeout / 1000
).toFixed(3)} seconds`; ).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'; message = 'RequestError: The daily request limit has been exceeded';
} else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) {
if (!error.request?.options?.headers?.authorization?.includes('-')) { if (!error?.request?.options?.headers?.authorization?.includes('-')) {
message = message =
'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} else { } else {
@ -229,14 +231,18 @@ export class GhostfolioService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = error; let message = error;
if (error.name === 'AbortError') { if (['AbortError', 'TimeoutError'].includes(error?.name)) {
message = `RequestError: The operation to get the quotes was aborted because the request to the data provider took more than ${( 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 requestTimeout / 1000
).toFixed(3)} seconds`; ).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'; message = 'RequestError: The daily request limit has been exceeded';
} else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { } else if (error?.response?.statusCode === StatusCodes.UNAUTHORIZED) {
if (!error.request?.options?.headers?.authorization?.includes('-')) { if (!error?.request?.options?.headers?.authorization?.includes('-')) {
message = message =
'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} else { } else {
@ -272,14 +278,16 @@ export class GhostfolioService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( message = `RequestError: The operation to search for ${query} was aborted because the request to the data provider took more than ${(
requestTimeout / 1000 requestTimeout / 1000
).toFixed(3)} seconds`; ).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'; message = 'RequestError: The daily request limit has been exceeded';
} else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) { } else if (error.response?.statusCode === StatusCodes.UNAUTHORIZED) {
if (!error.request?.options?.headers?.authorization?.includes('-')) { if (!error?.request?.options?.headers?.authorization?.includes('-')) {
message = message =
'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.'; 'RequestError: The provided API key is invalid. Please update it in the Settings section of the Admin Control panel.';
} else { } else {

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

@ -165,7 +165,7 @@ export class RapidApiService implements DataProviderInterface {
} catch (error) { } catch (error) {
let message = 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 ${( message = `RequestError: The operation was aborted because the request to the data provider took more than ${(
this.configurationService.get('REQUEST_TIMEOUT') / 1000 this.configurationService.get('REQUEST_TIMEOUT') / 1000
).toFixed(3)} seconds`; ).toFixed(3)} seconds`;

69
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -103,39 +103,46 @@ export class GfAdminMarketDataComponent
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public activeFilters: Filter[] = []; public activeFilters: Filter[] = [];
public allFilters: Filter[] = Object.keys(AssetSubClass) public allFilters: Filter[] = [
.filter((assetSubClass) => { ...Object.keys(AssetSubClass)
return assetSubClass !== 'CASH'; .filter((assetSubClass) => {
}) return assetSubClass !== 'CASH';
.map((assetSubClass) => { })
.map((assetSubClass) => {
return {
id: assetSubClass.toString(),
label: translate(assetSubClass),
type: 'ASSET_SUB_CLASS' as Filter['type']
};
}),
...Object.keys(DataSource).map((dataSource) => {
return { return {
id: assetSubClass.toString(), id: dataSource.toString(),
label: translate(assetSubClass), label: dataSource,
type: 'ASSET_SUB_CLASS' as Filter['type'] type: 'DATA_SOURCE' as Filter['type']
}; };
}) }),
.concat([ {
{ id: 'BENCHMARKS',
id: 'BENCHMARKS', label: $localize`Benchmarks`,
label: $localize`Benchmarks`, type: 'PRESET_ID' as Filter['type']
type: 'PRESET_ID' as Filter['type'] },
}, {
{ id: 'CURRENCIES',
id: 'CURRENCIES', label: $localize`Currencies`,
label: $localize`Currencies`, type: 'PRESET_ID' as Filter['type']
type: 'PRESET_ID' as Filter['type'] },
}, {
{ id: 'ETF_WITHOUT_COUNTRIES',
id: 'ETF_WITHOUT_COUNTRIES', label: $localize`ETFs without Countries`,
label: $localize`ETFs without Countries`, type: 'PRESET_ID' as Filter['type']
type: 'PRESET_ID' as Filter['type'] },
}, {
{ id: 'ETF_WITHOUT_SECTORS',
id: 'ETF_WITHOUT_SECTORS', label: $localize`ETFs without Sectors`,
label: $localize`ETFs without Sectors`, type: 'PRESET_ID' as Filter['type']
type: 'PRESET_ID' as Filter['type'] }
} ];
]);
public benchmarks: Partial<SymbolProfile>[]; public benchmarks: Partial<SymbolProfile>[];
public currentDataSource: DataSource; public currentDataSource: DataSource;
public currentSymbol: string; public currentSymbol: string;

1
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_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_OSS: $localize`Switch to Ghostfolio Premium easily`,
DATA_IMPORT_AND_EXPORT_TOOLTIP_PREMIUM: $localize`Switch to Ghostfolio Open Source or Ghostfolio Basic 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`, EMERGENCY_FUND: $localize`Emergency Fund`,
EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`, EXCLUDE_FROM_ANALYSIS: $localize`Exclude from Analysis`,
Global: $localize`Global`, Global: $localize`Global`,

12
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.192.0", "version": "2.193.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.192.0", "version": "2.193.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -93,7 +93,7 @@
"svgmap": "2.12.2", "svgmap": "2.12.2",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.23.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.4.1", "yahoo-finance2": "3.6.4",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {
@ -40601,9 +40601,9 @@
} }
}, },
"node_modules/yahoo-finance2": { "node_modules/yahoo-finance2": {
"version": "3.4.1", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.4.1.tgz", "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.6.4.tgz",
"integrity": "sha512-L8Ubmdsn6f+uJEuEDUUHR5n95TFcGkMiMkV0phmvPONFekAn1vWzsEzGfIDG2ODR7aYBB+aURdQg7a3HX2iUHA==", "integrity": "sha512-IoMU8Hb4BEaNPVnamZjRBuorTGDbaaiV/tM/m3KI8dzwrR6BGmeuT40OX+5IqRiSkMlD8g0kAwGi9E4bY3rLvg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@deno/shim-deno": "~0.18.0", "@deno/shim-deno": "~0.18.0",

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.192.0", "version": "2.193.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -139,7 +139,7 @@
"svgmap": "2.12.2", "svgmap": "2.12.2",
"twitter-api-v2": "1.23.0", "twitter-api-v2": "1.23.0",
"uuid": "11.1.0", "uuid": "11.1.0",
"yahoo-finance2": "3.4.1", "yahoo-finance2": "3.6.4",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
"devDependencies": { "devDependencies": {

Loading…
Cancel
Save